Compare commits

...

116 Commits

Author SHA1 Message Date
Jellyfin Release Bot 7bbdfc0d49 Bump version to 10.9.11 2024-09-07 18:10:49 -04:00
Bond-009 3c3ebe8344 Merge pull request #12575 from dmitrylyzo/fix-subtitleextract
Fix subtitle and attachment extraction when input path contains quotes
2024-09-07 22:07:05 +02:00
Dmitry Lyzo 8c4d23435e Fix attachment extraction when input path contains quotes 2024-09-04 00:53:57 +03:00
Dmitry Lyzo 9e4befa52e Fix subtitle extraction when input path contains quotes 2024-09-04 00:09:09 +03:00
Bond-009 b3efae71c0 Merge pull request #12562 from nyanmisaka/fix-profiles
Use filtered codecs to build appliedConditions
2024-09-03 09:20:27 +02:00
Bond-009 70f4f2e8c2 Merge pull request #12558 from Bond-009/altsourcename
Fix alt version name generation
2024-09-03 09:20:15 +02:00
Bond-009 cd2f2ca178 Merge pull request #12550 from Bond-009/formattingstreamwriter
Create and use FormattingStreamWriter
2024-09-03 09:19:32 +02:00
nyanmisaka 8095d39954 Use filtered codecs to build appliedConditions
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-09-01 21:44:23 +08:00
Bond_009 e1ac30ba17 Fix alt version name generation
Instead of replacing all occurrences of the containing folder name, just check the start of the string.
This matches what happens in VideoListResolver.IsEligibleForMultiVersion
Fixes #12555
2024-08-31 14:03:53 +02:00
Bond_009 3b94cfa837 Create and use FormattingStreamWriter
Prevents bugs causes by system cultures with different formatting
2024-08-30 17:19:02 +02:00
Niels van Velzen 2fe13f54ea Merge pull request #12531 from gnattu/dont-apply-chapter-image-settings-to-music
Don't apply chapter image settings to music
2024-08-29 09:43:43 +02:00
gnattu eb6a6d319b Don't apply chapter image settings to music
Music Album covers are usually not 16:9 and should not use the chapter image resolutions in any case.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-08-29 03:11:34 +08:00
Nyanmisaka b74c9cae1b Fix CodecProfiles and video encoder profiles (#12521)
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2024-08-28 12:42:44 -06:00
Jellyfin Release Bot 24d482b36b Bump version to 10.9.10 2024-08-25 02:34:36 -04:00
Dmitry Lyzo fff4477a93 Apply all codec conditions (#12499) 2024-08-23 07:54:12 -06:00
Fredrik Eriksson 9810d22d96 Revert "NextUp query respects Limit (#11956)" (#12414)
fix from PR #11956 leads to unexpected behaviour as
Fixes: #12367
2024-08-23 07:54:02 -06:00
ikelos 5027e3cd53 Include AVIF extension for support images (#12415) 2024-08-22 08:04:50 -06:00
Niels van Velzen 9645955629 Set Content-Disposition header to attachment for image endpoints (#12490) 2024-08-22 08:01:10 -06:00
Niels van Velzen 078ee1f2de Merge pull request #12493 from jellyfin/fix-ts-bsf
Fix bitstream filter not applied to videos in TS container
2024-08-22 15:50:56 +02:00
Nyanmisaka 236c7649dd Fix bitstream filter not applied to videos in TS container
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-08-21 22:52:08 +08:00
Bond-009 c1e52df0b7 Fix the record series button missing on many programs (port of #12398) (#12481)
Co-authored-by: grumpycat <bryan.pauquette@gmail.com>
2024-08-20 11:13:37 -06:00
Bond-009 be949af59e Merge pull request #12425 from scampower3/10.9.z-dont-force-virtual
Don't force non-virtual when all episodes in season are isMissing=true
2024-08-14 16:10:30 +02:00
Bond-009 122da8f447 Merge pull request #12443 from gnattu/check-attach-path-for-null
Check attachment path for null before use
2024-08-14 16:10:11 +02:00
gnattu 486c7fa51e Check attachment path for null before use
This one is particularly nasty because the `Path` is not supposed to be nullable. However, fixing it in the parser and type constructor is beyond the scope of the 10.9 release. For now, just check with `IsNullOrEmpty`.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-08-14 04:44:35 +08:00
LJQ 6709c80f0a Don't force non-virtual when all episodes in season have isMissing flag. 2024-08-11 18:06:09 +08:00
Niels van Velzen 3f3145600c Merge pull request #12390 from justinkb/fix-sa1201
fix SA1201 issue
2024-08-05 14:19:48 +02:00
Paul Mulders bc613b8344 fix SA1201 issue 2024-08-05 13:22:24 +02:00
Jellyfin Release Bot 0eb5897100 Bump version to 10.9.9 2024-08-04 22:01:35 -04:00
Nyanmisaka ee0094d889 Fix compatibility between TranscodingThrottler and FFmpeg 7.0 (#12374) 2024-08-04 20:00:57 -06:00
Bond-009 7051a18be0 Merge pull request #12368 from Bond-009/updateserilogasp
Update Serilog deps
2024-08-01 20:03:20 +02:00
Bond_009 fce3a5d241 Update Serilog deps 2024-08-01 16:40:13 +02:00
Niels van Velzen 900acc03aa Fix creating virtual seasons (again) (#12356) 2024-07-30 09:51:43 -06:00
renovate[bot] a475a7d50a Update dependency libse to v4.0.7 2024-07-24 09:35:40 -06:00
Joshua M. Boniface b7bc0e1c96 Merge pull request #11901 from gnattu/remove-efcore-secondlevelcache
Implement Device Cache to replace EFCoreSecondLevelCacheInterceptor
2024-07-21 01:19:35 -04:00
Jellyfin Release Bot 3c79d7a3f3 Bump version to 10.9.8 2024-07-21 01:11:35 -04:00
gnattu 1739962f52 Update Jellyfin.Server.Implementations/Devices/DeviceManager.cs
Co-authored-by: Bond-009 <bond.009@outlook.com>
2024-07-20 20:42:43 +08:00
gnattu 7f0f93eb4a Update Jellyfin.Server.Implementations/Devices/DeviceManager.cs
Co-authored-by: Bond-009 <bond.009@outlook.com>
2024-07-20 20:42:31 +08:00
gnattu 71c13057f4 Remove package reference of EFCoreSecondLevelCacheInterceptor
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:20:08 +08:00
gnattu 7f12677dc3 Directly add new device to cache
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 4f2b1736ab Add user not found message
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu c05049e54e Filter full device list only once
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu dd5f6406a2 Use FirstAsync for device creation
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 79c0a7d7f0 Don't cache user in DeviceManager, query from user cache instead
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 3c6485f0a1 Get device id from input
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 8a8b2c4380 Explicitly declare type of devices
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 7403428864 Always enumerate to get count
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 235da65a75 Use concrete ConcurrentDictionary Type
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 26eab7aa2e Remove env var for second level cache
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu d235378133 Query User on device creation
Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
gnattu 5a62c7a146 Implement Device Cache to replace EFCoreSecondLevelCacheInterceptor
The EFCoreSecondLevelCacheInterceptor will place a huge lock even for reading. Implement a ConcurrentDictionary cache to replace it.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-07-20 20:19:31 +08:00
Bond-009 4afa6db108 Properly escape paths in concat file for BDMV (#12296) 2024-07-17 12:33:11 -04:00
Bond-009 f7a90b6383 Merge pull request #12278 from Bond-009/localizeaudio
Fix localization of audio title
2024-07-16 14:45:13 +02:00
Bond_009 b8e2d8e11a Fix tests 2024-07-15 14:40:12 +02:00
Niels van Velzen c1f7ccbca4 Fix season handling ("Season Unknown" / unneccesary empty seasons) (#12240) 2024-07-15 08:27:19 -04:00
Bond_009 5bab02fa54 Fix localization of audio title
Maybe passing ILocalizationManager into the ctor of MediaStream is a better solution for master?
2024-07-15 14:27:12 +02:00
Bond-009 f2fa0b9025 Merge pull request #12246 from jellyfin/renovate/dotnet-monorepo
Update dotnet monorepo

(cherry picked from commit 5ef76a5e31)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2024-07-12 09:47:58 -04:00
Bond-009 9c0edd2905 Merge pull request #12259 from jellyfin/renovate/serilog.settings.configuration-8.x
Update dependency Serilog.Settings.Configuration to v8.0.2

(cherry picked from commit c050abf3e8)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2024-07-12 09:47:23 -04:00
Bond-009 62deebc04c Merge pull request #12248 from jellyfin/renovate/dotnet-monorepo
Update dotnet monorepo to v8.0.7

(cherry picked from commit f62af90ae3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2024-07-12 09:44:46 -04:00
Jellyfin Release Bot 478d8b07bf Bump version to 10.9.7 2024-06-24 20:19:28 -04:00
Bond-009 c9b6ebd94f Merge pull request #11911 from Bond-009/infoaudionorm
Log album name and id in normalization task
2024-06-24 22:51:57 +02:00
Bond-009 30fc089dd5 Merge pull request #12166 from Bond-009/4kbdmv
Fix HDR detection for 4K Blu-Ray BDMVs
2024-06-24 22:51:41 +02:00
Bond-009 25f02658f0 Merge pull request #12126 from gnattu/add-extracted-lyrics
Try to add extracted lyrics during scanning
2024-06-24 22:51:28 +02:00
Bond-009 2266a00337 Merge pull request #12055 from Shadowghost/fix-season-backdrops
Fix season backdrops
2024-06-24 22:41:57 +02:00
Shadowghost afeff31dca Merge remote-tracking branch 'upstream/release-10.9.z' into fix-season-backdrops 2024-06-24 22:34:43 +02:00
Bond-009 476dc01f4d Merge pull request #12025 from Shadowghost/remove-empty-image-folders-recursive
Fix empty image folder removal for legacy locations
2024-06-24 22:14:09 +02:00
Bond_009 b81b674ae1 Log album name and id in normalization task
Filename of the concat file is now the same as the album id.
Temp file gets deleted even if LUFS calculation failed
2024-06-24 22:01:49 +02:00
Bond_009 15eb7a25b9 Fix HDR detection for 4K Blu-Ray BDMVs 2024-06-24 11:43:01 +02:00
Shadowghost aadd57bc48 Fix check 2024-06-24 09:16:51 +02:00
Bond-009 cbbe5db813 Merge pull request #12053 from Shadowghost/fix-local-playlist-scanning
Rewrite PlaylistItemsProvider as ILocalMetadataProvider
2024-06-23 17:56:23 +02:00
Bond-009 4601097d3e Merge pull request #12050 from Shadowghost/fix-seasons
Fix season handling
2024-06-23 17:48:48 +02:00
gnattu 10cd9a7f79 Only add first stream
Signed-off-by: gnattu <gnattuoc@me.com>
2024-06-22 18:26:59 +08:00
Tim Eisele 6cf98d4930 Only cleanup children on specific exceptions (#12134) 2024-06-21 09:08:05 -06:00
Tim Eisele 34a65980e3 Remove incomplete mediatype restriction from playlists (#12024) 2024-06-21 09:07:38 -06:00
Nyanmisaka 6010bc01c3 Fix MicroDVD being recognized as DVDSUB subtitles (#12149) 2024-06-21 09:07:25 -06:00
Shadowghost a00f9e1a10 Cleanup seasons after creating real ones 2024-06-20 22:03:01 +02:00
Bond-009 85078d8f10 Merge pull request #12123 from Shadowghost/fix-cleanup-task
Fix Cleanup Task metadata saving
2024-06-20 11:36:48 +02:00
Bond-009 1606b6c0f6 Merge pull request #12043 from jellyfin/pr-parental-au-1
Fix the Australian PG rating
2024-06-20 11:20:19 +02:00
Bond-009 f097aad01e Merge pull request #12094 from Shadowghost/fix-dual-socket-address-handling
Map IPv6 mapped IPv4 addresses back to IPv4 before running checks
2024-06-20 11:20:06 +02:00
Tim Eisele bf53f1ae38 Do not override <year> if <releasedate> is set (#12120) 2024-06-18 08:16:21 -06:00
gnattu 31237f778a Try to add extracted lyrics during scanning
The extraction process does not add the extracted lyrics to the audio media streams. Try to add it when tryExtractEmbeddedLyrics is true.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-06-18 10:08:11 +08:00
Shadowghost 55d245a77b Fix saving item metadata 2024-06-17 23:13:53 +02:00
Odd Stråbø 9f35f56eaf Fix the Australian PG rating
As per https://www.classification.gov.au/classification-ratings/what-are-ratings

Fixes #11650
Well, sort of. I don't think it is possible to differentiate between them, as we'd be comparing the integer values, not the position in the list?
2024-06-15 19:55:14 +02:00
Bond-009 f2a5ccf102 Merge pull request #12065 from Rivenlalala/fix-bdmv-file-extension-case-issue
Make m2ts extension case-insensitive
2024-06-15 17:52:05 +02:00
Bond-009 2b78980747 Merge pull request #12017 from gnattu/overwrite-livetv-codec
Overwrite supported codecs for livetv
2024-06-15 17:38:10 +02:00
Bond-009 a89678074e Merge pull request #12026 from Bond-009/hisubs
Check hearing impared flags with equality instead of contains
2024-06-15 17:37:49 +02:00
Bond-009 d813f83b4a Merge pull request #12039 from Shadowghost/fix-local-episode-thumb
Fix local episode image thumb recognition
2024-06-15 17:37:30 +02:00
Bond-009 37b7e953f7 Merge pull request #12031 from jellyfin/fix-video-embedded-image
Fix video embedded image detection
2024-06-15 17:36:19 +02:00
Bond-009 08b64c5502 Merge pull request #12028 from Shadowghost/fix-replace
Fix replace all and respect metadata settings
2024-06-15 17:36:05 +02:00
Bond-009 23a660e917 Merge pull request #12073 from Shadowghost/fix-mb
Fix Music Brainz release group query
2024-06-15 17:34:44 +02:00
Bond-009 78eb9b2f78 Merge pull request #12046 from gnattu/fix-wrong-mpegts-detection
Fix mpeg-ts detection
2024-06-15 17:34:31 +02:00
Bond-009 d90f504ca7 Merge pull request #12037 from Shadowghost/fix-user-delete
Do not fail user deletion if we have no playlist folder
2024-06-15 17:34:21 +02:00
Shadowghost 56104d3042 Map IPv6 mapped IPv4 addresses back to IPv4 before running checks 2024-06-14 10:22:10 +02:00
Rivenlalala fcec1fcc4d Make m2ts extension case-insensitive 2024-06-12 04:16:50 +08:00
Shadowghost 1a14902da8 Apply review suggestion 2024-06-11 18:48:38 +02:00
Shadowghost 34bdf8bf78 Do not cleanup old backdrops 2024-06-11 10:29:06 +02:00
Shadowghost 7ff3f6af6c Fix MB release group query 2024-06-10 21:43:42 +02:00
Shadowghost bd8b0c4c03 Remove all existing backdrops when replacing all images 2024-06-09 23:17:57 +02:00
Shadowghost 0c560a313a Fix local season backdrop saving 2024-06-09 21:35:12 +02:00
Shadowghost 8882bb495c Rewrite PlaylistItemsProvider as ILocalMetadataProvider 2024-06-09 20:53:33 +02:00
Shadowghost e4078f984a Fix season handling 2024-06-09 18:47:21 +02:00
gnattu 8e5a2f565c Fix mpeg-ts detection
When the container name is `mpeg`, it means it is MPEG-PS, while the TS container should have the explicit name `mpeg-ts`.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-06-09 21:23:13 +08:00
Shadowghost 8b442a7749 Check for existence before trying to delete directory 2024-06-09 08:24:58 +02:00
Shadowghost b63f7a2bc0 Only remove image from item if file system delete was successful 2024-06-09 00:46:46 +02:00
Shadowghost f9e7d5229e Limit removal scope 2024-06-09 00:46:19 +02:00
Shadowghost b24d05bff7 Apply review suggestion 2024-06-08 22:35:50 +02:00
Shadowghost fd009fc71b Simplify metadata subdir check 2024-06-08 21:57:21 +02:00
Shadowghost 302eea1cb7 Fix local episode image thumb recognition 2024-06-08 21:51:08 +02:00
Shadowghost b116a2742e Do not fail user deletion if we have no playlist folder 2024-06-08 16:46:12 +02:00
nyanmisaka 99a04e23d9 Fix video embedded image detection
Fixes debbfaa. Embedded images also exist in video.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-06-08 16:55:27 +08:00
Shadowghost 19a89d5a60 Remove folder after removing empty subfolders 2024-06-08 00:12:36 +02:00
Shadowghost feb20c131a Use helper 2024-06-08 00:08:11 +02:00
Shadowghost ec82023265 Respect different metadata settings on refresh 2024-06-07 23:19:30 +02:00
Shadowghost e4f3f0b3b6 Remove all data when replacing all 2024-06-07 23:19:04 +02:00
Shadowghost 28274d4c75 Remove empty image folders recursively 2024-06-07 22:12:48 +02:00
Bond_009 b6595e4efc Check hearing impared flags with equality instead of contains
Fixes #12019
2024-06-07 22:12:35 +02:00
gnattu 4046ef1c13 Overwrite supported codecs for livetv
Only changeing streamingRequest is not enough. The internal logic will do codec shifting based on supported codecs, need to overwrite all of them.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-06-07 17:58:35 +08:00
72 changed files with 810 additions and 585 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "8.0.4", "version": "8.0.7",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]
+18 -19
View File
@@ -16,34 +16,33 @@
<PackageVersion Include="Diacritics" Version="3.3.29" /> <PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" /> <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.5" /> <PackageVersion Include="libse" Version="4.0.7" />
<PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.6" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.6" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.6" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
@@ -59,12 +58,12 @@
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.2.1" /> <PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" />
@@ -79,8 +78,8 @@
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.3" /> <PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" /> <PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
+1 -1
View File
@@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.9.6</VersionPrefix> <VersionPrefix>10.9.11</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -107,7 +107,7 @@ namespace Emby.Naming.ExternalFiles
pathInfo.Language = culture.ThreeLetterISOLanguageName; pathInfo.Language = culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
} }
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase))) else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
{ {
pathInfo.IsHearingImpaired = true; pathInfo.IsHearingImpaired = true;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations
{ FfmpegAnalyzeDurationKey, "200M" }, { FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString }, { PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString }, { BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" }, { SqliteCacheSizeKey, "20000" }
{ SqliteDisableSecondLevelCacheKey, bool.FalseString }
}; };
} }
} }
@@ -5694,13 +5694,17 @@ AND Type = @InternalPersonType)");
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result; item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
if (item.Type == MediaStreamType.Subtitle) if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{ {
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
item.LocalizedDefault = _localization.GetLocalizedString("Default"); item.LocalizedDefault = _localization.GetLocalizedString("Default");
item.LocalizedForced = _localization.GetLocalizedString("Forced");
item.LocalizedExternal = _localization.GetLocalizedString("External"); item.LocalizedExternal = _localization.GetLocalizedString("External");
item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
if (item.Type is MediaStreamType.Subtitle)
{
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
item.LocalizedForced = _localization.GetLocalizedString("Forced");
item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
}
} }
return item; return item;
@@ -389,7 +389,7 @@ namespace Emby.Server.Implementations.IO
var info = new FileInfo(path); var info = new FileInfo(path);
if (info.Exists && if (info.Exists &&
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden) (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
{ {
if (isHidden) if (isHidden)
{ {
@@ -417,8 +417,8 @@ namespace Emby.Server.Implementations.IO
return; return;
} }
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden) && (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
{ {
return; return;
} }
@@ -1884,7 +1884,7 @@ namespace Emby.Server.Implementations.Library
try try
{ {
var index = item.GetImageIndex(img); var index = item.GetImageIndex(img);
image = await ConvertImageToLocal(item, img, index, removeOnFailure: true).ConfigureAwait(false); image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@@ -1,11 +1,11 @@
Exempt,0 Exempt,0
G,0 G,0
7+,7 7+,7
PG,15
M,15 M,15
MA,15 MA,15
MA15+,15 MA15+,15
MA 15+,15 MA 15+,15
PG,16
16+,16 16+,16
R,18 R,18
R18+,18 R18+,18
1 Exempt 0
2 G 0
3 7+ 7
4 PG 15
5 M 15
6 MA 15
7 MA15+ 15
8 MA 15+ 15
PG 16
9 16+ 16
10 R 18
11 R18+ 18
@@ -170,8 +170,13 @@ namespace Emby.Server.Implementations.Playlists
private List<Playlist> GetUserPlaylists(Guid userId) private List<Playlist> GetUserPlaylists(Guid userId)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
var playlistsFolder = GetPlaylistsFolder(userId);
if (playlistsFolder is null)
{
return [];
}
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>().ToList(); return playlistsFolder.GetChildren(user, true).OfType<Playlist>().ToList();
} }
private static string GetTargetPath(string path) private static string GetTargetPath(string path)
@@ -184,11 +189,11 @@ namespace Emby.Server.Implementations.Playlists
return path; return path;
} }
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options) private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, User user, DtoOptions options)
{ {
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null); var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); return Playlist.GetPlaylistItems(items, user, options);
} }
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
@@ -208,7 +213,7 @@ namespace Emby.Server.Implementations.Playlists
?? throw new ArgumentException("No Playlist exists with Id " + playlistId); ?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
// Retrieve all the items to be added to the playlist // Retrieve all the items to be added to the playlist
var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options) var newItems = GetPlaylistItems(newItemIds, user, options)
.Where(i => i.SupportsAddingToPlaylist); .Where(i => i.SupportsAddingToPlaylist);
// Filter out duplicate items, if necessary // Filter out duplicate items, if necessary
@@ -106,13 +106,20 @@ public partial class AudioNormalizationTask : IScheduledTask
continue; continue;
} }
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat"); _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), a.Id + ".concat");
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
a.LUFS = await CalculateLUFSAsync( try
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), {
cancellationToken).ConfigureAwait(false); a.LUFS = await CalculateLUFSAsync(
File.Delete(tempFile); string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
cancellationToken).ConfigureAwait(false);
}
finally
{
File.Delete(tempFile);
}
} }
_itemRepository.SaveItems(albums, cancellationToken); _itemRepository.SaveItems(albums, cancellationToken);
@@ -127,15 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
{ {
_logger.LogDebug("Updating {FolderName}", folder.Name); _logger.LogDebug("Updating {FolderName}", folder.Name);
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
_providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken); folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
_providerManager.QueueRefresh(
folder.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ForceSave = true
},
RefreshPriority.High);
} }
} }
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
@@ -133,53 +134,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
DeleteFile(file.FullName); FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
index++; index++;
} }
DeleteEmptyFolders(directory); FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
progress.Report(100); progress.Report(100);
} }
private void DeleteEmptyFolders(string parent)
{
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
{
DeleteEmptyFolders(directory);
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
{
try
{
Directory.Delete(directory, false);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Error deleting directory {Path}", directory);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting directory {Path}", directory);
}
}
}
}
private void DeleteFile(string path)
{
try
{
_fileSystem.DeleteFile(path);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Error deleting file {Path}", path);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting file {Path}", path);
}
}
} }
} }
@@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
@@ -113,53 +113,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
DeleteFile(file.FullName); FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
index++; index++;
} }
DeleteEmptyFolders(directory); FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
progress.Report(100); progress.Report(100);
} }
private void DeleteEmptyFolders(string parent)
{
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
{
DeleteEmptyFolders(directory);
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
{
try
{
Directory.Delete(directory, false);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Error deleting directory {Path}", directory);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting directory {Path}", directory);
}
}
}
}
private void DeleteFile(string path)
{
try
{
_fileSystem.DeleteFile(path);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Error deleting file {Path}", path);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting file {Path}", path);
}
}
} }
} }
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(deviceId); ArgumentException.ThrowIfNullOrEmpty(deviceId);
var activityDate = DateTime.UtcNow; var activityDate = DateTime.UtcNow;
var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
var lastActivityDate = session.LastActivityDate; var lastActivityDate = session.LastActivityDate;
session.LastActivityDate = activityDate; session.LastActivityDate = activityDate;
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param> /// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param> /// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns> /// <returns>SessionInfo.</returns>
private async Task<SessionInfo> GetSessionInfo( private SessionInfo GetSessionInfo(
string appName, string appName,
string appVersion, string appVersion,
string deviceId, string deviceId,
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
if (!_activeConnections.TryGetValue(key, out var sessionInfo)) if (!_activeConnections.TryGetValue(key, out var sessionInfo))
{ {
sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
_activeConnections[key] = sessionInfo; _activeConnections[key] = sessionInfo;
} }
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
return sessionInfo; return sessionInfo;
} }
private async Task<SessionInfo> CreateSession( private SessionInfo CreateSession(
string key, string key,
string appName, string appName,
string appVersion, string appVersion,
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device"; deviceName = "Network Device";
} }
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false); var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
if (string.IsNullOrEmpty(deviceOptions.CustomName)) if (string.IsNullOrEmpty(deviceOptions.CustomName))
{ {
sessionInfo.DeviceName = deviceName; sessionInfo.DeviceName = deviceName;
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
return new[] { item }; return new[] { item };
} }
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user) private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
return new List<BaseItem>(); return new List<BaseItem>();
} }
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }); return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -1520,12 +1520,12 @@ namespace Emby.Server.Implementations.Session
// This should be validated above, but if it isn't don't delete all tokens. // This should be validated above, but if it isn't don't delete all tokens.
ArgumentException.ThrowIfNullOrEmpty(deviceId); ArgumentException.ThrowIfNullOrEmpty(deviceId);
var existing = (await _deviceManager.GetDevices( var existing = _deviceManager.GetDevices(
new DeviceQuery new DeviceQuery
{ {
DeviceId = deviceId, DeviceId = deviceId,
UserId = user.Id UserId = user.Id
}).ConfigureAwait(false)).Items; }).Items;
foreach (var auth in existing) foreach (var auth in existing)
{ {
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(accessToken); ArgumentException.ThrowIfNullOrEmpty(accessToken);
var existing = (await _deviceManager.GetDevices( var existing = _deviceManager.GetDevices(
new DeviceQuery new DeviceQuery
{ {
Limit = 1, Limit = 1,
AccessToken = accessToken AccessToken = accessToken
}).ConfigureAwait(false)).Items; }).Items;
if (existing.Count > 0) if (existing.Count > 0)
{ {
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
{ {
CheckDisposed(); CheckDisposed();
var existing = await _deviceManager.GetDevices(new DeviceQuery var existing = _deviceManager.GetDevices(new DeviceQuery
{ {
UserId = userId UserId = userId
}).ConfigureAwait(false); });
foreach (var info in existing.Items) foreach (var info in existing.Items)
{ {
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc /> /// <inheritdoc />
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{ {
var items = (await _deviceManager.GetDevices(new DeviceQuery var items = _deviceManager.GetDevices(new DeviceQuery
{ {
AccessToken = token, AccessToken = token,
Limit = 1 Limit = 1
}).ConfigureAwait(false)).Items; }).Items;
if (items.Count == 0) if (items.Count == 0)
{ {
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
} }
string? presentationUniqueKey = null; string? presentationUniqueKey = null;
int? limit = request.Limit; int? limit = null;
if (!request.SeriesId.IsNullOrEmpty()) if (!request.SeriesId.IsNullOrEmpty())
{ {
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series) if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
@@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId) public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false); return _deviceManager.GetDevicesForUser(userId);
} }
/// <summary> /// <summary>
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Info")] [HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id) public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
{ {
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false); var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo is null) if (deviceInfo is null)
{ {
return NotFound(); return NotFound();
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Options")] [HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id) public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
{ {
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false); var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo is null) if (deviceInfo is null)
{ {
return NotFound(); return NotFound();
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id) public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
{ {
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false); var existingDevice = _deviceManager.GetDevice(id);
if (existingDevice is null) if (existingDevice is null)
{ {
return NotFound(); return NotFound();
} }
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false); var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
foreach (var session in sessions.Items) foreach (var session in sessions.Items)
{ {
@@ -2089,6 +2089,8 @@ public class ImageController : BaseJellyfinApiController
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept); Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
Response.Headers.ContentDisposition = "attachment";
if (disableCaching) if (disableCaching)
{ {
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
@@ -80,7 +80,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| imageRefreshMode == MetadataRefreshMode.FullRefresh || imageRefreshMode == MetadataRefreshMode.FullRefresh
|| replaceAllImages || replaceAllImages
|| replaceAllMetadata, || replaceAllMetadata,
IsAutomated = false IsAutomated = false,
RemoveOldMetadata = replaceAllMetadata
}; };
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High); _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
@@ -233,6 +233,8 @@ public class PluginsController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
Response.Headers.ContentDisposition = "attachment";
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
} }
@@ -95,6 +95,7 @@ public class TrickplayController : BaseJellyfinApiController
var path = _trickplayManager.GetTrickplayTilePath(item, width, index); var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
if (System.IO.File.Exists(path)) if (System.IO.File.Exists(path))
{ {
Response.Headers.ContentDisposition = "attachment";
return PhysicalFile(path, MediaTypeNames.Image.Jpeg); return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
} }
+5
View File
@@ -154,6 +154,11 @@ public static class StreamingHelpers
// Some channels from HDHomerun will experience A/V sync issues // Some channels from HDHomerun will experience A/V sync issues
streamingRequest.SegmentContainer = "ts"; streamingRequest.SegmentContainer = "ts";
streamingRequest.VideoCodec = "h264"; streamingRequest.VideoCodec = "h264";
streamingRequest.AudioCodec = "aac";
state.SupportedVideoCodecs = ["h264"];
state.Request.VideoCodec = "h264";
state.SupportedAudioCodecs = ["aac"];
state.Request.AudioCodec = "aac";
} }
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false); var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
+1 -1
View File
@@ -18,7 +18,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId> <PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.9.6</VersionPrefix> <VersionPrefix>10.9.11</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new(); private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
private readonly ConcurrentDictionary<int, Device> _devices;
private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DeviceManager"/> class. /// Initializes a new instance of the <see cref="DeviceManager"/> class.
@@ -37,6 +39,23 @@ namespace Jellyfin.Server.Implementations.Devices
{ {
_dbProvider = dbProvider; _dbProvider = dbProvider;
_userManager = userManager; _userManager = userManager;
_devices = new ConcurrentDictionary<int, Device>();
_deviceOptions = new ConcurrentDictionary<string, DeviceOptions>();
using var dbContext = _dbProvider.CreateDbContext();
foreach (var device in dbContext.Devices
.OrderBy(d => d.Id)
.AsEnumerable())
{
_devices.TryAdd(device.Id, device);
}
foreach (var deviceOption in dbContext.DeviceOptions
.OrderBy(d => d.Id)
.AsEnumerable())
{
_deviceOptions.TryAdd(deviceOption.DeviceId, deviceOption);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -66,6 +85,8 @@ namespace Jellyfin.Server.Implementations.Devices
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
_deviceOptions[deviceId] = deviceOptions;
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions))); DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
} }
@@ -76,25 +97,17 @@ namespace Jellyfin.Server.Implementations.Devices
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
dbContext.Devices.Add(device); dbContext.Devices.Add(device);
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
_devices.TryAdd(device.Id, device);
} }
return device; return device;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<DeviceOptions> GetDeviceOptions(string deviceId) public DeviceOptions GetDeviceOptions(string deviceId)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); _deviceOptions.TryGetValue(deviceId, out var deviceOptions);
DeviceOptions? deviceOptions;
await using (dbContext.ConfigureAwait(false))
{
deviceOptions = await dbContext.DeviceOptions
.AsNoTracking()
.FirstOrDefaultAsync(d => d.DeviceId == deviceId)
.ConfigureAwait(false);
}
return deviceOptions ?? new DeviceOptions(deviceId); return deviceOptions ?? new DeviceOptions(deviceId);
} }
@@ -108,57 +121,43 @@ namespace Jellyfin.Server.Implementations.Devices
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<DeviceInfo?> GetDevice(string id) public DeviceInfo? GetDevice(string id)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
await using (dbContext.ConfigureAwait(false)) _deviceOptions.TryGetValue(id, out var deviceOption);
{
var device = await dbContext.Devices
.Where(d => d.DeviceId == id)
.OrderByDescending(d => d.DateLastActivity)
.Include(d => d.User)
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
.FirstOrDefaultAsync()
.ConfigureAwait(false);
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options); var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
return deviceInfo;
return deviceInfo;
}
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<QueryResult<Device>> GetDevices(DeviceQuery query) public QueryResult<Device> GetDevices(DeviceQuery query)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); IEnumerable<Device> devices = _devices.Values
await using (dbContext.ConfigureAwait(false)) .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
.OrderBy(d => d.Id)
.ToList();
var count = devices.Count();
if (query.Skip.HasValue)
{ {
var devices = dbContext.Devices devices = devices.Skip(query.Skip.Value);
.OrderBy(d => d.Id)
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
var count = await devices.CountAsync().ConfigureAwait(false);
if (query.Skip.HasValue)
{
devices = devices.Skip(query.Skip.Value);
}
if (query.Limit.HasValue)
{
devices = devices.Take(query.Limit.Value);
}
return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
} }
if (query.Limit.HasValue)
{
devices = devices.Take(query.Limit.Value);
}
return new QueryResult<Device>(query.Skip, count, devices.ToList());
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query) public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
{ {
var devices = await GetDevices(query).ConfigureAwait(false); var devices = GetDevices(query);
return new QueryResult<DeviceInfo>( return new QueryResult<DeviceInfo>(
devices.StartIndex, devices.StartIndex,
@@ -167,38 +166,36 @@ namespace Jellyfin.Server.Implementations.Devices
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId) public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); IEnumerable<Device> devices = _devices.Values
await using (dbContext.ConfigureAwait(false)) .OrderByDescending(d => d.DateLastActivity)
.ThenBy(d => d.DeviceId);
if (!userId.IsNullOrEmpty())
{ {
var sessions = dbContext.Devices var user = _userManager.GetUserById(userId.Value);
.Include(d => d.User) if (user is null)
.OrderByDescending(d => d.DateLastActivity)
.ThenBy(d => d.DeviceId)
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
.AsAsyncEnumerable();
if (!userId.IsNullOrEmpty())
{ {
var user = _userManager.GetUserById(userId.Value); throw new ResourceNotFoundException();
if (user is null)
{
throw new ResourceNotFoundException();
}
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
} }
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false); devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
return new QueryResult<DeviceInfo>(array);
} }
var array = devices.Select(device =>
{
_deviceOptions.TryGetValue(device.DeviceId, out var option);
return ToDeviceInfo(device, option);
}).ToArray();
return new QueryResult<DeviceInfo>(array);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DeleteDevice(Device device) public async Task DeleteDevice(Device device)
{ {
_devices.TryRemove(device.Id, out _);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
@@ -207,6 +204,19 @@ namespace Jellyfin.Server.Implementations.Devices
} }
} }
/// <inheritdoc />
public async Task UpdateDevice(Device device)
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Devices.Update(device);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
_devices[device.Id] = device;
}
/// <inheritdoc /> /// <inheritdoc />
public bool CanAccessDevice(User user, string deviceId) public bool CanAccessDevice(User user, string deviceId)
{ {
@@ -225,6 +235,11 @@ namespace Jellyfin.Server.Implementations.Devices
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null) private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
{ {
var caps = GetCapabilities(authInfo.DeviceId); var caps = GetCapabilities(authInfo.DeviceId);
var user = _userManager.GetUserById(authInfo.UserId);
if (user is null)
{
throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
}
return new DeviceInfo return new DeviceInfo
{ {
@@ -232,7 +247,7 @@ namespace Jellyfin.Server.Implementations.Devices
AppVersion = authInfo.AppVersion, AppVersion = authInfo.AppVersion,
Id = authInfo.DeviceId, Id = authInfo.DeviceId,
LastUserId = authInfo.UserId, LastUserId = authInfo.UserId,
LastUserName = authInfo.User.Username, LastUserName = user.Username,
Name = authInfo.DeviceName, Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity, DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps.IconUrl, IconUrl = caps.IconUrl,
@@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -16,28 +15,13 @@ public static class ServiceCollectionExtensions
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled. /// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
/// </summary> /// </summary>
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param> /// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
/// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
/// <returns>The updated service collection.</returns> /// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache) public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
{ {
if (!disableSecondLevelCache)
{
serviceCollection.AddEFSecondLevelCache(options =>
options.UseMemoryCacheProvider()
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
.UseCacheKeyPrefix("EF_")
// Don't cache null values. Remove this optional setting if it's not necessary.
.SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
}
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) => serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{ {
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>(); var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}"); opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
if (!disableSecondLevelCache)
{
dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
}
}); });
return serviceCollection; return serviceCollection;
@@ -27,7 +27,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AsyncKeyedLock" /> <PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
<PackageReference Include="System.Linq.Async" /> <PackageReference Include="System.Linq.Async" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
@@ -4,7 +4,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Queries;
using Jellyfin.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
{ {
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider; private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDeviceManager _deviceManager;
private readonly IServerApplicationHost _serverApplicationHost; private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext( public AuthorizationContext(
IDbContextFactory<JellyfinDbContext> jellyfinDb, IDbContextFactory<JellyfinDbContext> jellyfinDb,
IUserManager userManager, IUserManager userManager,
IDeviceManager deviceManager,
IServerApplicationHost serverApplicationHost) IServerApplicationHost serverApplicationHost)
{ {
_jellyfinDbProvider = jellyfinDb; _jellyfinDbProvider = jellyfinDb;
_userManager = userManager; _userManager = userManager;
_deviceManager = deviceManager;
_serverApplicationHost = serverApplicationHost; _serverApplicationHost = serverApplicationHost;
} }
@@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); var device = _deviceManager.GetDevices(
new DeviceQuery
{
AccessToken = token
}).Items.FirstOrDefault();
if (device is not null) if (device is not null)
{ {
@@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
if (updateToken) if (updateToken)
{ {
dbContext.Devices.Update(device); await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
} }
else else
@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
private async Task UpdateDeviceAccess(User user) private async Task UpdateDeviceAccess(User user)
{ {
var existing = (await _deviceManager.GetDevices(new DeviceQuery var existing = _deviceManager.GetDevices(new DeviceQuery
{ {
UserId = user.Id UserId = user.Id
}).ConfigureAwait(false)).Items; }).Items;
foreach (var device in existing) foreach (var device in existing)
{ {
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
} }
}) })
.UseStartup(_ => new Startup(appHost, startupConfig)); .UseStartup(_ => new Startup(appHost));
} }
} }
+2 -5
View File
@@ -40,18 +40,15 @@ namespace Jellyfin.Server
{ {
private readonly CoreAppHost _serverApplicationHost; private readonly CoreAppHost _serverApplicationHost;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IConfiguration _startupConfig;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class. /// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary> /// </summary>
/// <param name="appHost">The server application host.</param> /// <param name="appHost">The server application host.</param>
/// <param name="startupConfig">The server startupConfig.</param> public Startup(CoreAppHost appHost)
public Startup(CoreAppHost appHost, IConfiguration startupConfig)
{ {
_serverApplicationHost = appHost; _serverApplicationHost = appHost;
_serverConfigurationManager = appHost.ConfigurationManager; _serverConfigurationManager = appHost.ConfigurationManager;
_startupConfig = startupConfig;
} }
/// <summary> /// <summary>
@@ -70,7 +67,7 @@ namespace Jellyfin.Server
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371 // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>(); services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled()); services.AddJellyfinDbContext();
services.AddJellyfinApiSwagger(); services.AddJellyfinApiSwagger();
// configure custom legacy authentication // configure custom legacy authentication
@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId> <PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.9.6</VersionPrefix> <VersionPrefix>10.9.11</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
/// </summary> /// </summary>
/// <param name="id">The identifier.</param> /// <param name="id">The identifier.</param>
/// <returns>DeviceInfo.</returns> /// <returns>DeviceInfo.</returns>
Task<DeviceInfo> GetDevice(string id); DeviceInfo GetDevice(string id);
/// <summary> /// <summary>
/// Gets devices based on the provided query. /// Gets devices based on the provided query.
/// </summary> /// </summary>
/// <param name="query">The device query.</param> /// <param name="query">The device query.</param>
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns> /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
Task<QueryResult<Device>> GetDevices(DeviceQuery query); QueryResult<Device> GetDevices(DeviceQuery query);
Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query); QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
/// <summary> /// <summary>
/// Gets the devices. /// Gets the devices.
/// </summary> /// </summary>
/// <param name="userId">The user's id, or <c>null</c>.</param> /// <param name="userId">The user's id, or <c>null</c>.</param>
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns> /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId); QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
Task DeleteDevice(Device device); Task DeleteDevice(Device device);
Task UpdateDevice(Device device);
/// <summary> /// <summary>
/// Determines whether this instance [can access device] the specified user identifier. /// Determines whether this instance [can access device] the specified user identifier.
/// </summary> /// </summary>
@@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
Task UpdateDeviceOptions(string deviceId, string deviceName); Task UpdateDeviceOptions(string deviceId, string deviceName);
Task<DeviceOptions> GetDeviceOptions(string deviceId); DeviceOptions GetDeviceOptions(string deviceId);
} }
} }
+13 -11
View File
@@ -1180,28 +1180,29 @@ namespace MediaBrowser.Controller.Entities
return info; return info;
} }
private string GetMediaSourceName(BaseItem item) internal string GetMediaSourceName(BaseItem item)
{ {
var terms = new List<string>(); var terms = new List<string>();
var path = item.Path; var path = item.Path;
if (item.IsFileProtocol && !string.IsNullOrEmpty(path)) if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
{ {
var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
if (HasLocalAlternateVersions) if (HasLocalAlternateVersions)
{ {
var displayName = System.IO.Path.GetFileNameWithoutExtension(path) var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
.Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase) if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName, StringComparison.OrdinalIgnoreCase))
.TrimStart(new char[] { ' ', '-' });
if (!string.IsNullOrEmpty(displayName))
{ {
terms.Add(displayName); var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
if (!name.IsWhiteSpace())
{
terms.Add(name.ToString());
}
} }
} }
if (terms.Count == 0) if (terms.Count == 0)
{ {
var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
terms.Add(displayName); terms.Add(displayName);
} }
} }
@@ -1949,14 +1950,15 @@ namespace MediaBrowser.Controller.Entities
return; return;
} }
// Remove it from the item // Remove from file system
RemoveImage(info);
if (info.IsLocalFile) if (info.IsLocalFile)
{ {
FileSystem.DeleteFile(info.Path); FileSystem.DeleteFile(info.Path);
} }
// Remove from item
RemoveImage(info);
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
} }
+11 -1
View File
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -370,9 +371,18 @@ namespace MediaBrowser.Controller.Entities
{ {
nonCachedChildren = GetNonCachedChildren(directoryService); nonCachedChildren = GetNonCachedChildren(directoryService);
} }
catch (IOException ex)
{
Logger.LogError(ex, "Error retrieving children from file system");
}
catch (SecurityException ex)
{
Logger.LogError(ex, "Error retrieving children from file system");
}
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error retrieving children folder"); Logger.LogError(ex, "Error retrieving children");
return;
} }
progress.Report(ProgressHelpers.RetrievedChildren); progress.Report(ProgressHelpers.RetrievedChildren);
@@ -180,10 +180,7 @@ namespace MediaBrowser.Controller.Entities.TV
} }
public string FindSeriesPresentationUniqueKey() public string FindSeriesPresentationUniqueKey()
{ => Series?.PresentationUniqueKey;
var series = Series;
return series is null ? null : series.PresentationUniqueKey;
}
public string FindSeasonName() public string FindSeasonName()
{ {
@@ -430,8 +430,6 @@ namespace MediaBrowser.Controller.Entities
InternalItemsQuery query, InternalItemsQuery query,
ILibraryManager libraryManager) ILibraryManager libraryManager)
{ {
var user = query.User;
// This must be the last filter // This must be the last filter
if (!query.AdjacentTo.IsNullOrEmpty()) if (!query.AdjacentTo.IsNullOrEmpty())
{ {
@@ -64,11 +64,6 @@ namespace MediaBrowser.Controller.Extensions
/// </summary> /// </summary>
public const string SqliteCacheSizeKey = "sqlite:cacheSize"; public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
/// Disable second level cache of sqlite.
/// </summary>
public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
/// <summary> /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>. /// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary> /// </summary>
@@ -133,15 +128,5 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The sqlite cache size.</returns> /// <returns>The sqlite cache size.</returns>
public static int? GetSqliteCacheSize(this IConfiguration configuration) public static int? GetSqliteCacheSize(this IConfiguration configuration)
=> configuration.GetValue<int?>(SqliteCacheSizeKey); => configuration.GetValue<int?>(SqliteCacheSizeKey);
/// <summary>
/// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
/// <returns>Whether second level cache disabled.</returns>
public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
{
return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
}
} }
} }
@@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Linq;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.IO;
/// <summary>
/// Helper methods for file system management.
/// </summary>
public static class FileSystemHelper
{
/// <summary>
/// Deletes the file.
/// </summary>
/// <param name="fileSystem">The fileSystem.</param>
/// <param name="path">The path.</param>
/// <param name="logger">The logger.</param>
public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
{
try
{
fileSystem.DeleteFile(path);
}
catch (UnauthorizedAccessException ex)
{
logger.LogError(ex, "Error deleting file {Path}", path);
}
catch (IOException ex)
{
logger.LogError(ex, "Error deleting file {Path}", path);
}
}
/// <summary>
/// Recursively delete empty folders.
/// </summary>
/// <param name="fileSystem">The fileSystem.</param>
/// <param name="path">The path.</param>
/// <param name="logger">The logger.</param>
public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
{
foreach (var directory in fileSystem.GetDirectoryPaths(path))
{
DeleteEmptyFolders(fileSystem, directory, logger);
if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
{
try
{
Directory.Delete(directory, false);
}
catch (UnauthorizedAccessException ex)
{
logger.LogError(ex, "Error deleting directory {Path}", directory);
}
catch (IOException ex)
{
logger.LogError(ex, "Error deleting directory {Path}", directory);
}
}
}
}
}
@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId> <PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.9.6</VersionPrefix> <VersionPrefix>10.9.11</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -1208,8 +1208,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subtitlePath = state.SubtitleStream.Path; var subtitlePath = state.SubtitleStream.Path;
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase) // dvdsub/vobsub graphical subtitles use .sub+.idx pairs
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase)) if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
{ {
var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
if (File.Exists(idxFile)) if (File.Exists(idxFile))
@@ -1313,7 +1313,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Apply aac_adtstoasc bitstream filter when media source is in mpegts. // Apply aac_adtstoasc bitstream filter when media source is in mpegts.
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase) && (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase) || string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))) || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{ {
@@ -2005,7 +2005,26 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty; var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
profile = WhiteSpaceRegex().Replace(profile, string.Empty); profile = WhiteSpaceRegex().Replace(profile, string.Empty).ToLowerInvariant();
var videoProfiles = Array.Empty<string>();
if (string.Equals("h264", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
{
videoProfiles = _videoProfilesH264;
}
else if (string.Equals("hevc", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
{
videoProfiles = _videoProfilesH265;
}
else if (string.Equals("av1", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
{
videoProfiles = _videoProfilesAv1;
}
if (!videoProfiles.Contains(profile, StringComparison.OrdinalIgnoreCase))
{
profile = string.Empty;
}
// We only transcode to HEVC 8-bit for now, force Main Profile. // We only transcode to HEVC 8-bit for now, force Main Profile.
if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase) if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
@@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query); return base.GetChildren(user, true, query);
} }
public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options) public static IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
{ {
if (user is not null) if (user is not null)
{ {
@@ -177,14 +177,14 @@ namespace MediaBrowser.Controller.Playlists
foreach (var item in inputItems) foreach (var item in inputItems)
{ {
var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options); var playlistItems = GetPlaylistItems(item, user, options);
list.AddRange(playlistItems); list.AddRange(playlistItems);
} }
return list; return list;
} }
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options) private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, DtoOptions options)
{ {
if (item is MusicGenre musicGenre) if (item is MusicGenre musicGenre)
{ {
@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Playlists
{ {
Recursive = true, Recursive = true,
IsFolder = false, IsFolder = false,
MediaTypes = [mediaType], MediaTypes = [MediaType.Audio, MediaType.Video],
EnableTotalRecordCount = false, EnableTotalRecordCount = false,
DtoOptions = options DtoOptions = options
}; };
@@ -40,13 +40,12 @@ namespace MediaBrowser.LocalMetadata.Images
var parentPathFiles = directoryService.GetFiles(parentPath); var parentPathFiles = directoryService.GetFiles(parentPath);
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString(); var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
var thumbName = string.Concat(nameWithoutExtension, "-thumb"); var images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
var images = GetImageFilesFromFolder(thumbName, parentPathFiles);
var metadataSubPath = directoryService.GetDirectories(parentPath).Where(d => d.Name.EndsWith("metadata", StringComparison.OrdinalIgnoreCase)).ToList(); var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
foreach (var path in metadataSubPath) if (metadataSubDir is not null)
{ {
var files = directoryService.GetFiles(path.FullName); var files = directoryService.GetFiles(metadataSubDir.FullName);
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files)); images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
} }
@@ -55,9 +54,8 @@ namespace MediaBrowser.LocalMetadata.Images
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths) private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
{ {
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1); var list = new List<LocalImageInfo>(1);
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
foreach (var i in filePaths) foreach (var i in filePaths)
{ {
@@ -89,7 +89,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath, string outputPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)); var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
if (shouldExtractOneByOne) if (shouldExtractOneByOne)
{ {
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index); var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
@@ -283,7 +284,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
if (extractableAttachmentIds.Count > 0) if (extractableAttachmentIds.Count > 0)
{ {
await CacheAllAttachmentsInternal(mediaPath, inputFile, mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false); await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -322,7 +323,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
processArgs += string.Format( processArgs += string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
" -i \"{0}\" -t 0 -f null null", " -i {0} -t 0 -f null null",
inputFile); inputFile);
int exitCode; int exitCode;
@@ -168,6 +168,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly string _encoderPath; private readonly string _encoderPath;
private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
public EncoderValidator(ILogger logger, string encoderPath) public EncoderValidator(ILogger logger, string encoderPath)
{ {
_logger = logger; _logger = logger;
@@ -477,7 +479,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false; return false;
} }
public bool CheckSupportedRuntimeKey(string keyDesc) public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
{ {
if (string.IsNullOrEmpty(keyDesc)) if (string.IsNullOrEmpty(keyDesc))
{ {
@@ -487,7 +489,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output; string output;
try try
{ {
output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?"); // With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -193,7 +193,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_threads = EncodingHelper.GetNumberOfThreads(null, options, null); _threads = EncodingHelper.GetNumberOfThreads(null, options, null);
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding"); _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
// Check the Vaapi device vendor // Check the Vaapi device vendor
if (OperatingSystem.IsLinux() if (OperatingSystem.IsLinux()
@@ -625,7 +625,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
try try
{ {
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, cancellationToken).ConfigureAwait(false); return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, false, cancellationToken).ConfigureAwait(false);
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@@ -637,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
} }
} }
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false); return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, isAudio, cancellationToken).ConfigureAwait(false);
} }
private string GetImageResolutionParameter() private string GetImageResolutionParameter()
@@ -663,7 +663,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
return imageResolutionParameter; return imageResolutionParameter;
} }
private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken) private async Task<string> ExtractImageInternal(
string inputPath,
string container,
MediaStream videoStream,
int? imageStreamIndex,
Video3DFormat? threedFormat,
TimeSpan? offset,
bool useIFrame,
ImageFormat? targetFormat,
bool isAudio,
CancellationToken cancellationToken)
{ {
ArgumentException.ThrowIfNullOrEmpty(inputPath); ArgumentException.ThrowIfNullOrEmpty(inputPath);
@@ -722,7 +732,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var vf = string.Join(',', filters); var vf = string.Join(',', filters);
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, GetImageResolutionParameter()); var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, isAudio ? string.Empty : GetImageResolutionParameter());
if (offset.HasValue) if (offset.HasValue)
{ {
@@ -1155,10 +1165,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Get all files from the BDMV/STREAMING directory // Get all files from the BDMV/STREAMING directory
// Only return playable local .m2ts files // Only return playable local .m2ts files
var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList();
return validPlaybackFiles return validPlaybackFiles
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f))) .Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName)
.Where(f => f.Exists) .Where(f => f is not null)
.Select(f => f.FullName)
.ToList(); .ToList();
} }
@@ -1197,7 +1207,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
} }
// Generate concat configuration entries for each file and write to file // Generate concat configuration entries for each file and write to file
using StreamWriter sw = new StreamWriter(concatFilePath); Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
using StreamWriter sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
foreach (var path in files) foreach (var path in files)
{ {
var mediaInfoResult = GetMediaInfo( var mediaInfoResult = GetMediaInfo(
@@ -1216,7 +1227,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds; var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
// Add file path stanza to concat configuration // Add file path stanza to concat configuration
sw.WriteLine("file '{0}'", path); sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
// Add duration stanza to concat configuration // Add duration stanza to concat configuration
sw.WriteLine("duration {0}", duration); sw.WriteLine("duration {0}", duration);
@@ -280,8 +280,8 @@ namespace MediaBrowser.MediaEncoding.Probing
splitFormat[i] = "mpeg"; splitFormat[i] = "mpeg";
} }
// Handle MPEG-2 container // Handle MPEG-TS container
else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(splitFormat[i], "mpegts", StringComparison.OrdinalIgnoreCase))
{ {
splitFormat[i] = "ts"; splitFormat[i] = "ts";
} }
@@ -624,15 +624,19 @@ namespace MediaBrowser.MediaEncoding.Probing
{ {
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase)) if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
{ {
codec = "dvbsub"; codec = "DVBSUB";
} }
else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(codec, "dvb_teletext", StringComparison.OrdinalIgnoreCase))
{ {
codec = "PGSSUB"; codec = "DVBTXT";
} }
else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(codec, "dvd_subtitle", StringComparison.OrdinalIgnoreCase))
{ {
codec = "DVDSUB"; codec = "DVDSUB"; // .sub+.idx
}
else if (string.Equals(codec, "hdmv_pgs_subtitle", StringComparison.OrdinalIgnoreCase))
{
codec = "PGSSUB"; // .sup
} }
return codec; return codec;
@@ -717,6 +721,8 @@ namespace MediaBrowser.MediaEncoding.Probing
if (streamInfo.CodecType == CodecType.Audio) if (streamInfo.CodecType == CodecType.Audio)
{ {
stream.Type = MediaStreamType.Audio; stream.Type = MediaStreamType.Audio;
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.Channels = streamInfo.Channels; stream.Channels = streamInfo.Channels;
@@ -779,11 +785,10 @@ namespace MediaBrowser.MediaEncoding.Probing
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase); && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
if (isAudio if (isAudio
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
{ {
stream.Type = MediaStreamType.EmbeddedImage; stream.Type = MediaStreamType.EmbeddedImage;
} }
@@ -501,11 +501,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
List<MediaStream> subtitleStreams, List<MediaStream> subtitleStreams,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var inputPath = mediaSource.Path; var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
var outputPaths = new List<string>(); var outputPaths = new List<string>();
var args = string.Format( var args = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"-i \"{0}\" -copyts", "-i {0} -copyts",
inputPath); inputPath);
foreach (var subtitleStream in subtitleStreams) foreach (var subtitleStream in subtitleStreams)
@@ -676,7 +676,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var processArgs = string.Format( var processArgs = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"", "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
inputPath, inputPath,
subtitleStreamIndex, subtitleStreamIndex,
outputCodec, outputCodec,
@@ -51,6 +51,8 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
o.PoolInitialFill = 1; o.PoolInitialFill = 1;
}); });
private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TranscodeManager"/> class. /// Initializes a new instance of the <see cref="TranscodeManager"/> class.
/// </summary> /// </summary>
@@ -559,7 +561,9 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
private void StartThrottler(StreamState state, TranscodingJob transcodingJob) private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
{ {
if (EnableThrottling(state)) if (EnableThrottling(state)
&& (_mediaEncoder.IsPkeyPauseSupported
|| _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
{ {
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder); transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
transcodingJob.TranscodingThrottler.Start(); transcodingJob.TranscodingThrottler.Start();
+15 -15
View File
@@ -858,7 +858,7 @@ namespace MediaBrowser.Model.Dlna
{ {
audioStream = directAudioStream; audioStream = directAudioStream;
playlistItem.AudioStreamIndex = audioStream.Index; playlistItem.AudioStreamIndex = audioStream.Index;
playlistItem.AudioCodecs = new[] { audioStream.Codec }; playlistItem.AudioCodecs = audioCodecs = new[] { audioStream.Codec };
// Copy matching audio codec options // Copy matching audio codec options
playlistItem.AudioSampleRate = audioStream.SampleRate; playlistItem.AudioSampleRate = audioStream.SampleRate;
@@ -897,18 +897,18 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video && .Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(videoStream?.Codec, container) && i.ContainsAnyCodec(videoCodecs, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
var isFirstAppliedCodecProfile = true; // Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
foreach (var i in appliedVideoConditions) foreach (var i in appliedVideoConditions)
{ {
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec); foreach (var transcodingVideoCodec in videoCodecs)
foreach (var transcodingVideoCodec in transcodingVideoCodecs)
{ {
if (i.ContainsAnyCodec(transcodingVideoCodec, container)) if (i.ContainsAnyCodec(transcodingVideoCodec, container))
{ {
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile); ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
isFirstAppliedCodecProfile = false;
continue; continue;
} }
} }
@@ -929,18 +929,18 @@ namespace MediaBrowser.Model.Dlna
var appliedAudioConditions = options.Profile.CodecProfiles var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio && .Where(i => i.Type == CodecType.VideoAudio &&
i.ContainsAnyCodec(audioStream?.Codec, container) && i.ContainsAnyCodec(audioCodecs, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
isFirstAppliedCodecProfile = true; // Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
foreach (var codecProfile in appliedAudioConditions) foreach (var codecProfile in appliedAudioConditions)
{ {
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); foreach (var transcodingAudioCodec in audioCodecs)
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
{ {
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
{ {
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
isFirstAppliedCodecProfile = false;
break; break;
} }
} }
+7 -7
View File
@@ -656,14 +656,14 @@ namespace MediaBrowser.Model.Entities
{ {
string codec = format ?? string.Empty; string codec = format ?? string.Empty;
// sub = external .sub file // microdvd and dvdsub/vobsub share the ".sub" file extension, but it's text-based.
return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase)
&& !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) || (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) && !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase) && !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase); && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase));
} }
public bool SupportsSubtitleConversionTo(string toCodec) public bool SupportsSubtitleConversionTo(string toCodec)
+1 -1
View File
@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId> <PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.9.6</VersionPrefix> <VersionPrefix>10.9.11</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
+26 -7
View File
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@@ -188,11 +189,27 @@ namespace MediaBrowser.Providers.Manager
{ {
_fileSystem.DeleteFile(currentPath); _fileSystem.DeleteFile(currentPath);
// Remove containing directory if empty // Remove local episode metadata directory if it exists and is empty
var folder = Path.GetDirectoryName(currentPath); var directory = Path.GetDirectoryName(currentPath);
if (!_fileSystem.GetFiles(folder).Any()) if (item is Episode && directory.Equals("metadata", StringComparison.Ordinal))
{ {
Directory.Delete(folder); var parentDirectoryPath = Directory.GetParent(currentPath).FullName;
if (_fileSystem.DirectoryExists(parentDirectoryPath) && !_fileSystem.GetFiles(parentDirectoryPath).Any())
{
try
{
_logger.LogInformation("Deleting empty local metadata folder {Folder}", parentDirectoryPath);
Directory.Delete(parentDirectoryPath);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
}
}
} }
} }
catch (FileNotFoundException) catch (FileNotFoundException)
@@ -410,13 +427,15 @@ namespace MediaBrowser.Providers.Manager
if (type == ImageType.Backdrop && saveLocally) if (type == ImageType.Backdrop && saveLocally)
{ {
if (season is not null && season.IndexNumber.HasValue) if (season is not null
&& season.IndexNumber.HasValue
&& (imageIndex is null || imageIndex == 0))
{ {
var seriesFolder = season.SeriesPath; var seriesFolder = season.SeriesPath;
var seasonMarker = season.IndexNumber.Value == 0 var seasonMarker = season.IndexNumber.Value == 0
? "-specials" ? "-specials"
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-fanart" + extension; var imageFilename = "season" + seasonMarker + "-fanart" + extension;
@@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Manager
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions) public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
{ {
var hasChanges = false; var hasChanges = false;
IDirectoryService directoryService = refreshOptions?.DirectoryService; var directoryService = refreshOptions?.DirectoryService;
if (item is not Photo) if (item is not Photo)
{ {
@@ -158,7 +159,7 @@ namespace MediaBrowser.Providers.Manager
} }
} }
// only delete existing multi-images if new ones were added // Only delete existing multi-images if new ones were added
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count()) if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
{ {
PruneImages(item, oldBackdropImages); PruneImages(item, oldBackdropImages);
@@ -359,10 +360,8 @@ namespace MediaBrowser.Providers.Manager
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images) private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
{ {
for (var i = 0; i < images.Count; i++) foreach (var image in images)
{ {
var image = images[i];
if (image.IsLocalFile) if (image.IsLocalFile)
{ {
try try
@@ -377,19 +376,20 @@ namespace MediaBrowser.Providers.Manager
{ {
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path); _logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
} }
finally
{
// Always remove empty parent folder
var folder = Path.GetDirectoryName(image.Path);
if (Directory.Exists(folder) && !_fileSystem.GetFiles(folder).Any())
{
Directory.Delete(folder);
}
}
} }
} }
item.RemoveImages(images); item.RemoveImages(images);
// Cleanup old metadata directory for episodes if empty
if (item is Episode)
{
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
{
Directory.Delete(oldLocalMetadataDirectory);
}
}
} }
/// <summary> /// <summary>
@@ -422,14 +422,11 @@ namespace MediaBrowser.Providers.Manager
{ {
var changed = item.ValidateImages(); var changed = item.ValidateImages();
var foundImageTypes = new List<ImageType>(); var foundImageTypes = new List<ImageType>();
for (var i = 0; i < _singularImages.Length; i++) for (var i = 0; i < _singularImages.Length; i++)
{ {
var type = _singularImages[i]; var type = _singularImages[i];
var image = GetFirstLocalImageInfoByType(images, type); var image = GetFirstLocalImageInfoByType(images, type);
if (image is not null)
// Only use local images if we are not replacing and saving
if (image is not null && !(item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages))
{ {
var currentImage = item.GetImageInfo(type, 0); var currentImage = item.GetImageInfo(type, 0);
// if image file is stored with media, don't replace that later // if image file is stored with media, don't replace that later
@@ -92,10 +92,6 @@ namespace MediaBrowser.Providers.Manager
} }
} }
var localImagesFailed = false;
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages) if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
{ {
if (ImageProvider.RemoveImages(item)) if (ImageProvider.RemoveImages(item))
@@ -104,19 +100,29 @@ namespace MediaBrowser.Providers.Manager
} }
} }
// Start by validating images var localImagesFailed = false;
try var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
// Only validate already registered images if we are replacing and saving locally
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
{ {
// Always validate images and check for new locally stored ones. item.ValidateImages();
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
{
updateType |= ItemUpdateType.ImageUpdate;
}
} }
catch (Exception ex) else
{ {
localImagesFailed = true; // Run full image validation and register new local images
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name"); try
{
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
{
updateType |= ItemUpdateType.ImageUpdate;
}
}
catch (Exception ex)
{
localImagesFailed = true;
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
}
} }
var metadataResult = new MetadataResult<TItemType> var metadataResult = new MetadataResult<TItemType>
@@ -669,6 +675,8 @@ namespace MediaBrowser.Providers.Manager
}; };
temp.Item.Path = item.Path; temp.Item.Path = item.Path;
temp.Item.Id = item.Id; temp.Item.Id = item.Id;
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
var foundImageTypes = new List<ImageType>(); var foundImageTypes = new List<ImageType>();
@@ -811,19 +819,16 @@ namespace MediaBrowser.Providers.Manager
{ {
var refreshResult = new RefreshResult(); var refreshResult = new RefreshResult();
var tmpDataMerged = false; if (id is not null)
{
MergeNewData(temp.Item, id);
}
foreach (var provider in providers) foreach (var provider in providers)
{ {
var providerName = provider.GetType().Name; var providerName = provider.GetType().Name;
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName); Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
if (id is not null && !tmpDataMerged)
{
MergeNewData(temp.Item, id);
tmpDataMerged = true;
}
try try
{ {
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false); var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@@ -136,6 +137,10 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.IsLocked) if (!audio.IsLocked)
{ {
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false); await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
if (tryExtractEmbeddedLyrics)
{
AddExternalLyrics(audio, mediaStreams, options);
}
} }
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric); audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
@@ -369,7 +374,10 @@ namespace MediaBrowser.Providers.MediaInfo
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false); var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray(); audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
currentStreams.AddRange(externalLyricFiles); if (externalLyricFiles.Count > 0)
{
currentStreams.Add(externalLyricFiles[0]);
}
} }
} }
} }
@@ -358,6 +358,10 @@ namespace MediaBrowser.Providers.MediaInfo
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate; blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width; blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height; blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange;
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
} }
} }
@@ -1,7 +1,5 @@
#nullable disable #nullable disable
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -18,182 +16,212 @@ using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PlaylistsNET.Content; using PlaylistsNET.Content;
namespace MediaBrowser.Providers.Playlists namespace MediaBrowser.Providers.Playlists;
/// <summary>
/// Local playlist provider.
/// </summary>
public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
IHasOrder,
IForcedProvider,
IHasItemChangeMonitor
{ {
public class PlaylistItemsProvider : ICustomMetadataProvider<Playlist>, private readonly IFileSystem _fileSystem;
IHasOrder, private readonly ILibraryManager _libraryManager;
IForcedProvider, private readonly ILogger<PlaylistItemsProvider> _logger;
IPreRefreshProvider, private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
IHasItemChangeMonitor
/// <summary>
/// Initializes a new instance of the <see cref="PlaylistItemsProvider"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistItemsProvider}"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
{ {
private readonly IFileSystem _fileSystem; _logger = logger;
private readonly ILibraryManager _libraryManager; _libraryManager = libraryManager;
private readonly ILogger<PlaylistItemsProvider> _logger; _fileSystem = fileSystem;
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists]; }
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem) /// <inheritdoc />
public string Name => "Playlist Item Provider";
/// <inheritdoc />
public int Order => 100;
/// <inheritdoc />
public Task<MetadataResult<Playlist>> GetMetadata(
ItemInfo info,
IDirectoryService directoryService,
CancellationToken cancellationToken)
{
var result = new MetadataResult<Playlist>()
{ {
_logger = logger; Item = new Playlist
_libraryManager = libraryManager; {
_fileSystem = fileSystem; Path = info.Path
}
};
Fetch(result);
return Task.FromResult(result);
}
private void Fetch(MetadataResult<Playlist> result)
{
var item = result.Item;
var path = item.Path;
if (!Playlist.IsPlaylistFile(path))
{
return;
} }
public string Name => "Playlist Reader"; var extension = Path.GetExtension(path);
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
// Run last
public int Order => 100;
public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
var path = item.Path; return;
if (!Playlist.IsPlaylistFile(path)) }
{
return Task.FromResult(ItemUpdateType.None);
}
var extension = Path.GetExtension(path);
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(ItemUpdateType.None);
}
var items = GetItems(path, extension).ToArray();
var items = GetItems(path, extension).ToArray();
if (items.Length > 0)
{
result.HasMetadata = true;
item.LinkedChildren = items; item.LinkedChildren = items;
return Task.FromResult(ItemUpdateType.MetadataImport);
} }
private IEnumerable<LinkedChild> GetItems(string path, string extension) return;
}
private IEnumerable<LinkedChild> GetItems(string path, string extension)
{
var libraryRoots = _libraryManager.GetUserRootFolder().Children
.OfType<CollectionFolder>()
.Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
.SelectMany(f => f.PhysicalLocations)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
using (var stream = File.OpenRead(path))
{ {
var libraryRoots = _libraryManager.GetUserRootFolder().Children if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
.OfType<CollectionFolder>()
.Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
.SelectMany(f => f.PhysicalLocations)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
using (var stream = File.OpenRead(path))
{ {
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) return GetWplItems(stream, path, libraryRoots);
{
return GetWplItems(stream, path, libraryRoots);
}
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
{
return GetZplItems(stream, path, libraryRoots);
}
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
{
return GetM3uItems(stream, path, libraryRoots);
}
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
{
return GetM3uItems(stream, path, libraryRoots);
}
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
{
return GetPlsItems(stream, path, libraryRoots);
}
} }
return Enumerable.Empty<LinkedChild>(); if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
}
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new PlsContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new M3uContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new ZplContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new WplContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
{
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
{ {
return new LinkedChild return GetZplItems(stream, path, libraryRoots);
{
Path = parsedPath,
Type = LinkedChildType.Manual
};
} }
return null; if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
{
return GetM3uItems(stream, path, libraryRoots);
}
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
{
return GetM3uItems(stream, path, libraryRoots);
}
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
{
return GetPlsItems(stream, path, libraryRoots);
}
} }
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path) return Enumerable.Empty<LinkedChild>();
}
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new PlsContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new M3uContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new ZplContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
{
var content = new WplContent();
var playlist = content.GetFromStream(stream);
return playlist.PlaylistEntries
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
.Where(i => i is not null);
}
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
{
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
{ {
path = null; return new LinkedChild
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
if (!File.Exists(pathToCheck))
{ {
return false; Path = parsedPath,
} Type = LinkedChildType.Manual
};
}
foreach (var libraryPath in libraryPaths) return null;
{ }
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
{
path = pathToCheck;
return true;
}
}
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
{
path = null;
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
if (!File.Exists(pathToCheck))
{
return false; return false;
} }
public bool HasChanged(BaseItem item, IDirectoryService directoryService) foreach (var libraryPath in libraryPaths)
{ {
var path = item.Path; if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{ {
var file = directoryService.GetFile(path); path = pathToCheck;
if (file is not null && file.LastWriteTimeUtc != item.DateModified) return true;
{
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
return true;
}
} }
return false;
} }
return false;
}
/// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
var path = item.Path;
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
{
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
return true;
}
}
return false;
} }
} }
@@ -250,7 +250,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
// If we have a release ID but not a release group ID, lookup the release group // If we have a release ID but not a release group ID, lookup the release group
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
{ {
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false); var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
releaseGroupId = release.ReleaseGroup?.Id.ToString(); releaseGroupId = release.ReleaseGroup?.Id.ToString();
result.HasMetadata = true; result.HasMetadata = true;
} }
@@ -91,7 +91,7 @@ namespace MediaBrowser.Providers.TV
private void RemoveObsoleteSeasons(Series series) private void RemoveObsoleteSeasons(Series series)
{ {
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync. // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
var physicalSeasonNumbers = new HashSet<int>(); var physicalSeasonNumbers = new HashSet<int>();
var virtualSeasons = new List<Season>(); var virtualSeasons = new List<Season>();
foreach (var existingSeason in series.Children.OfType<Season>()) foreach (var existingSeason in series.Children.OfType<Season>())
@@ -203,11 +203,20 @@ namespace MediaBrowser.Providers.TV
foreach (var seasonNumber in uniqueSeasonNumbers) foreach (var seasonNumber in uniqueSeasonNumbers)
{ {
// Null season numbers will have a 'dummy' season created because seasons are always required. // Null season numbers will have a 'dummy' season created because seasons are always required.
if (!seasons.Any(i => i.IndexNumber == seasonNumber)) var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
if (existingSeason is null)
{ {
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
series.AddChild(season); }
else if (existingSeason.IsVirtualItem)
{
var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
if (episodeCount > 0)
{
existingSeason.IsVirtualItem = false;
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
} }
} }
} }
@@ -220,7 +229,7 @@ namespace MediaBrowser.Providers.TV
/// <param name="seasonNumber">The season number.</param> /// <param name="seasonNumber">The season number.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The newly created season.</returns> /// <returns>The newly created season.</returns>
private async Task<Season> CreateSeasonAsync( private async Task CreateSeasonAsync(
Series series, Series series,
string? seasonName, string? seasonName,
int? seasonNumber, int? seasonNumber,
@@ -237,14 +246,12 @@ namespace MediaBrowser.Providers.TV
typeof(Season)), typeof(Season)),
IsVirtualItem = false, IsVirtualItem = false,
SeriesId = series.Id, SeriesId = series.Id,
SeriesName = series.Name SeriesName = series.Name,
SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
}; };
series.AddChild(season); series.AddChild(season);
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
return season;
} }
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber) private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
@@ -519,7 +519,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate)) if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
{ {
item.PremiereDate = releaseDate; item.PremiereDate = releaseDate;
item.ProductionYear = releaseDate.Year;
// Production year can already be set by the year tag
item.ProductionYear ??= releaseDate.Year;
} }
break; break;
+2 -2
View File
@@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("10.9.6")] [assembly: AssemblyVersion("10.9.11")]
[assembly: AssemblyFileVersion("10.9.6")] [assembly: AssemblyFileVersion("10.9.11")]
+2 -1
View File
@@ -102,7 +102,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
"astc", "astc",
"ktx", "ktx",
"pkm", "pkm",
"wbmp" "wbmp",
"avif"
}; };
/// <inheritdoc /> /// <inheritdoc />
@@ -0,0 +1,38 @@
using System;
using System.IO;
namespace Jellyfin.Extensions;
/// <summary>
/// A custom StreamWriter which supports setting a IFormatProvider.
/// </summary>
public class FormattingStreamWriter : StreamWriter
{
private readonly IFormatProvider _formatProvider;
/// <summary>
/// Initializes a new instance of the <see cref="FormattingStreamWriter"/> class.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="formatProvider">The format provider to use.</param>
public FormattingStreamWriter(Stream stream, IFormatProvider formatProvider)
: base(stream)
{
_formatProvider = formatProvider;
}
/// <summary>
/// Initializes a new instance of the <see cref="FormattingStreamWriter"/> class.
/// </summary>
/// <param name="path">The complete file path to write to.</param>
/// <param name="formatProvider">The format provider to use.</param>
public FormattingStreamWriter(string path, IFormatProvider formatProvider)
: base(path)
{
_formatProvider = formatProvider;
}
/// <inheritdoc />
public override IFormatProvider FormatProvider
=> _formatProvider;
}
@@ -15,7 +15,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId> <PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.9.6</VersionPrefix> <VersionPrefix>10.9.11</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -167,7 +167,7 @@ namespace Jellyfin.LiveTv.Listings
Overview = program.Description, Overview = program.Description,
ProductionYear = program.CopyrightDate?.Year, ProductionYear = program.CopyrightDate?.Year,
SeasonNumber = program.Episode.Series, SeasonNumber = program.Episode.Series,
IsSeries = program.Episode.Series is not null, IsSeries = program.Episode.Episode is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not null, IsPremiere = program.Premiere is not null,
IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
@@ -919,10 +919,14 @@ public class NetworkManager : INetworkManager, IDisposable
{ {
ArgumentNullException.ThrowIfNull(address); ArgumentNullException.ThrowIfNull(address);
// See conversation at https://github.com/jellyfin/jellyfin/pull/3515. // Map IPv6 mapped IPv4 back to IPv4 (happens if Kestrel runs in dual-socket mode)
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
|| address.Equals(IPAddress.Loopback) || IPAddress.IsLoopback(address))
|| address.Equals(IPAddress.IPv6Loopback))
{ {
return true; return true;
} }
@@ -1,4 +1,7 @@
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
using Moq;
using Xunit; using Xunit;
namespace Jellyfin.Controller.Tests.Entities; namespace Jellyfin.Controller.Tests.Entities;
@@ -14,4 +17,30 @@ public class BaseItemTests
[InlineData("1test 2", "0000000001test 0000000002")] [InlineData("1test 2", "0000000001test 0000000002")]
public void BaseItem_ModifySortChunks_Valid(string input, string expected) public void BaseItem_ModifySortChunks_Valid(string input, string expected)
=> Assert.Equal(expected, BaseItem.ModifySortChunks(input)); => Assert.Equal(expected, BaseItem.ModifySortChunks(input));
[Theory]
[InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")]
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
{
var mediaSourceManager = new Mock<IMediaSourceManager>();
mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
.Returns((string x) => MediaProtocol.File);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
var video = new Video()
{
Path = primaryPath
};
var videoAlt = new Video()
{
Path = altPath,
};
video.LocalAlternateVersions = [videoAlt.Path];
Assert.Equal(name, video.GetMediaSourceName(video));
Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
}
} }
@@ -0,0 +1,23 @@
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using Xunit;
namespace Jellyfin.Extensions.Tests;
public static class FormattingStreamWriterTests
{
[Fact]
public static void Shuffle_Valid_Correct()
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE", false);
using (var ms = new MemoryStream())
using (var txt = new FormattingStreamWriter(ms, CultureInfo.InvariantCulture))
{
txt.Write("{0}", 3.14159);
txt.Close();
Assert.Equal("3.14159", Encoding.UTF8.GetString(ms.ToArray()));
}
}
}
@@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests
} }
[Theory] [Theory]
[InlineData(EncoderValidatorTestsData.FFmpegV701Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
@@ -33,6 +34,7 @@ namespace Jellyfin.MediaEncoding.Tests
{ {
public GetFFmpegVersionTestData() public GetFFmpegVersionTestData()
{ {
Add(EncoderValidatorTestsData.FFmpegV701Output, new Version(7, 0, 1));
Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1)); Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1));
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0)); Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2)); Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
{ {
internal static class EncoderValidatorTestsData internal static class EncoderValidatorTestsData
{ {
public const string FFmpegV701Output = @"ffmpeg version 7.0.1-Jellyfin Copyright (c) 2000-2024 the FFmpeg developers
built with clang version 18.1.8
configuration: --cc=clang --pkg-config-flags=--static --extra-cflags=-I/clang64/ffbuild/include --extra-ldflags=-L/clang64/ffbuild/lib --prefix=/clang64/ffbuild/jellyfin-ffmpeg --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --enable-lto=thin --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libharfbuzz --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libopenmpt --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libvpl --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
libavutil 59. 8.100 / 59. 8.100
libavcodec 61. 3.100 / 61. 3.100
libavformat 61. 1.100 / 61. 1.100
libavdevice 61. 1.100 / 61. 1.100
libavfilter 10. 1.100 / 10. 1.100
libswscale 8. 1.100 / 8. 1.100
libswresample 5. 1.100 / 5. 1.100
libpostproc 58. 1.100 / 58. 1.100";
public const string FFmpegV611Output = @"ffmpeg version n6.1.1-16-g33efa50fa4-20240317 Copyright (c) 2000-2023 the FFmpeg developers public const string FFmpegV611Output = @"ffmpeg version n6.1.1-16-g33efa50fa4-20240317 Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41) built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41)
configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libaribcaption --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317 configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libaribcaption --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317
@@ -18,7 +18,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
public class ProbeResultNormalizerTests public class ProbeResultNormalizerTests
{ {
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions;
private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null); private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), new Mock<ILocalizationManager>().Object);
public ProbeResultNormalizerTests() public ProbeResultNormalizerTests()
{ {
@@ -104,6 +104,7 @@ public class ExternalPathParserTests
[InlineData(".en.cc.title", "title", "eng", false, false, true)] [InlineData(".en.cc.title", "title", "eng", false, false, true)]
[InlineData(".hi.en.title", "title", "eng", false, false, true)] [InlineData(".hi.en.title", "title", "eng", false, false, true)]
[InlineData(".en.hi.title", "title", "eng", false, false, true)] [InlineData(".en.hi.title", "title", "eng", false, false, true)]
[InlineData(".Subs for Chinese Audio.eng", "Subs for Chinese Audio", "eng", false, false, false)]
public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false, bool isHearingImpaired = false) public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false, bool isHearingImpaired = false)
{ {
var path = "My.Video" + tokens + ".srt"; var path = "My.Video" + tokens + ".srt";