Compare commits

...

299 Commits

Author SHA1 Message Date
a936f76a12 Add path to log message 2024-09-08 15:13:17 -04:00
203af39e26 combine shared code 2024-09-08 14:40:06 -04:00
96b133b7b4 Sort files by path and add logging 2024-09-08 14:08:55 -04:00
34c63a4c15 fix type mismatch build error 2024-09-08 12:56:22 -04:00
4bb0259097 Add a check in the infinite loop 2024-09-08 12:41:48 -04:00
9fba024add nix stuff 2024-09-08 12:36:57 -04:00
Jellyfin Release Bot
7bbdfc0d49 Bump version to 10.9.11
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
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
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
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
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
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
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
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
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
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
Jellyfin Release Bot
b25d6d1e48 Bump version to 10.9.6
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2024-06-06 14:41:10 -04:00
Bond-009
cf59140276
Merge pull request #11959 from Shadowghost/continue-validation-if-removed
Do not stop validation if folder was removed
2024-06-06 20:02:38 +02:00
Bond-009
cc4563a477
Use only 1 write connection/DB (#11986) 2024-06-06 08:04:51 -06:00
gnattu
0d984b5162
Fix fallback artist when taglib fails (#11989) 2024-06-06 08:04:33 -06:00
Tim Eisele
279cba008b
Set ProductionLocations instead of Tags (#11984) 2024-06-06 06:47:15 -06:00
Jellyfin Release Bot
2b5d458456 Bump version to 10.9.5
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2024-06-05 18:04:17 -04:00
Joshua M. Boniface
f41efb3b2c
Merge pull request #11978 from Shadowghost/do-to-fallback-to-media-dir
Fallback to local dir when saving to media dir fails
2024-06-05 17:49:57 -04:00
Shadowghost
0155293c64 Fallback to local dir when saving to media dir fails 2024-06-05 23:39:13 +02:00
Joshua M. Boniface
b78efd6b1e
Merge pull request #11963 from gnattu/fix-rename-lib
Fix Library renaming
2024-06-05 17:30:56 -04:00
Joshua M. Boniface
bfcc09db8a
Merge pull request #11921 from Shadowghost/fix-identify-over-nfo
Fix identify over NFO and replace all when NFO saving enabled
2024-06-05 17:24:36 -04:00
Joshua M. Boniface
a46c17e19f
Merge pull request #11969 from Bond-009/readconns
Create readonly DB connections when possible
2024-06-05 17:09:34 -04:00
Tim Eisele
b0bb22b650
Fix local image saving (#11934) 2024-06-05 15:08:12 -06:00
Joshua M. Boniface
0c039145e5
Merge pull request #11935 from Shadowghost/fix-movie-nfo
Fix dateadded and movie NFO recognition
2024-06-05 17:01:17 -04:00
Joshua M. Boniface
2a3c904a9f
Merge pull request #11943 from Shadowghost/increase-migration-batch-size
Increase lyrics migration batch size to 5000
2024-06-05 17:00:31 -04:00
Shadowghost
7cbdb6708b Fix windows test 2024-06-05 22:15:51 +02:00
Shadowghost
7058db2b04 Fix test 2024-06-05 20:57:19 +02:00
Shadowghost
8f7df590cd Do not replace locked fields 2024-06-05 19:16:14 +02:00
Bond_009
0f67a5ba2f Actually create readonly connection when asked 2024-06-05 10:43:40 +02:00
Bond_009
19fb00b5b7 Revert "remove readonly"
This reverts commit e7016e38b8.
2024-06-05 10:43:40 +02:00
gnattu
8683253c6d Fix Library renaming
This handler should not just spawn a normal library validation task because our new logic will prevent the removal of library root folder unless explicitly required, which will cause the old lib still "ghosting" in the db.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-06-05 08:49:33 +08:00
Shadowghost
918a36d564 Fix test 2024-06-05 01:12:47 +02:00
Shadowghost
57ae0b5796 Fix typo 2024-06-05 00:58:00 +02:00
Shadowghost
262f7dd98f Fix metadata saver check 2024-06-05 00:53:09 +02:00
Shadowghost
4714b3af67 Ignore local images when replacing and saving is enabled 2024-06-05 00:26:30 +02:00
Shadowghost
0359035000 Do not stop validation if folder was removed 2024-06-04 23:14:45 +02:00
Shadowghost
b14edb8876 Do not run local providers if replacing and saving is enabled 2024-06-04 21:54:04 +02:00
Niels van Velzen
47c5e0c2c7
Merge pull request #11958 from Shadowghost/fix-trailer-nfo-saving
Export trailer URLs in new format
2024-06-04 21:33:11 +02:00
Shadowghost
8709d94783 Export trailer URLs in new format 2024-06-04 19:28:18 +02:00
Tim Eisele
23b1251393
Do not delete file locations for virtual episodes and seasons (#11954) 2024-06-04 07:09:20 -06:00
cptn
484aea1cdb
NextUp query respects Limit (#11956) 2024-06-04 07:05:32 -06:00
Niels van Velzen
d1c00ba4ed
Merge pull request #11920 from Shadowghost/fix-season-path
Only set season path if season folder parsing was successful
2024-06-04 12:48:51 +02:00
Niels van Velzen
b1a5fe2f55
Merge pull request #11933 from Shadowghost/fix-trailer-duplication
Check trailer distinction by URL
2024-06-04 12:04:39 +02:00
Shadowghost
d7ff6d023c Apply review suggestions 2024-06-03 16:39:22 +02:00
Shadowghost
253e95dcba Increase lyrics migration fetch batch size to 5000 2024-06-03 13:53:34 +02:00
Shadowghost
c7ce1aa4c7 Fix dateadded and movie NFO recognition 2024-06-02 22:33:02 +02:00
Shadowghost
3d87885577 Check trailer distinction by URL 2024-06-02 21:31:31 +02:00
Shadowghost
a7e2271845 Skip local metadata providers when identifying 2024-06-02 09:17:43 +02:00
Shadowghost
2a02abee46 Only set season path if season folder parsing was successful 2024-06-02 08:59:41 +02:00
Jellyfin Release Bot
ab4315742f Bump version to 10.9.4
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2024-06-01 18:38:59 -04:00
Joshua M. Boniface
2ddb15c784
Merge pull request #11743 from Shadowghost/fix-replace
Fix replace logic
2024-06-01 18:33:31 -04:00
Joshua M. Boniface
95c7d997c1
Merge pull request #11823 from gnattu/env-disable-second-level-cache
Add Env Var to disable second level cache
2024-06-01 18:32:54 -04:00
Joshua M. Boniface
e2c909f50f
Merge pull request #11762 from Shadowghost/fix-lyrics
Mark Audio as RequiresDeserialization and backfill data
2024-06-01 18:32:36 -04:00
Joshua M. Boniface
a53ea029fa
Merge pull request #11719 from Shadowghost/fix-season-names
Move NFO series season name parsing to own local provider
2024-06-01 18:31:55 -04:00
Joshua M. Boniface
869dab2ba2
Merge pull request #11873 from thornbill/fix-first-time-setup-guest-but-for-real
Fix FirstTimeSetupHandler allowing public access
2024-06-01 18:31:31 -04:00
Joshua M. Boniface
d2be2ee480
Merge pull request #11910 from Bond-009/audionormout
Audio normalization: parse ffmpeg output line by line
2024-06-01 16:07:05 -04:00
Bond_009
bc8ef94f0d Audio normalization: parse ffmpeg output line by line
Should prevent OOM error
Also optimized the regex so it can bail out earlier
2024-06-01 17:50:12 +02:00
Bill Thornton
ed1b880359 Remove api key check and simplify conditions 2024-05-31 16:31:15 -04:00
Bill Thornton
7221e7ca68 Fix FirstTimeSetupHandler failing for users and update tests 2024-05-31 14:09:04 -04:00
gnattu
0392daa103
Relax remuxing requirement for LiveTV (#11851) 2024-05-31 07:01:47 -06:00
gnattu
d602b6dbc5
Fix multi-part album folder being detected as artist folder (#11886) 2024-05-31 07:01:28 -06:00
Cody Robibero
b8a0cf6a9e
Merge pull request #11859 from gnattu/use-ffprobe-audio-metadata-fallback 2024-05-31 07:00:56 -06:00
Cody Robibero
26419c64f5
Merge pull request #11894 from gnattu/escape-concated-path 2024-05-31 06:58:00 -06:00
Bill Thornton
a71e2d9f0a
Update FirstTimeSetupHandler.cs
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-31 03:18:33 -04:00
gnattu
cfe67ff17d
Add extra white space
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-31 15:14:29 +08:00
gnattu
78e3ee15f9
Don't use interpolated strings
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-31 14:59:57 +08:00
gnattu
2cb74e3dd0 Escape tmpConcatPath for DVD and BD folder
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-31 14:54:00 +08:00
Joshua M. Boniface
8e979bdb4b
Merge pull request #11882 from Shadowghost/fix-season-missing-episode
Fix missing episodes query for seasons
2024-05-30 19:52:20 -04:00
Shadowghost
e099fd6141 Fix missing episodes query for seasons 2024-05-30 20:26:26 +02:00
Bill Thornton
35962bcc42 Fix FirstTimeSetupHandler api key test 2024-05-30 12:08:52 -04:00
Tim Eisele
ae584beaac
Return missing episodes for series when no user defined (#11806) 2024-05-30 09:32:37 -06:00
Cody Robibero
563033786f
Merge pull request #11876 from Bond-009/enableLib 2024-05-30 09:32:00 -06:00
Joshua M. Boniface
f8c7f36a34
Merge pull request #11867 from Shadowghost/stable-dep-upgrade
Upgrade dependencies
2024-05-30 10:36:30 -04:00
Shadowghost
4746c88633 Apply review suggestion 2024-05-30 09:21:23 +02:00
Shadowghost
b7d6bedbbb Apply review suggestion 2024-05-30 08:54:44 +02:00
Bond_009
8db79c05dd Don't check if admin has access to library when updating
The access check also checks if the library is enabled, this makes it impossible to enable disabled libraries.
Regression from  #11171
2024-05-29 22:27:29 +02:00
Bill Thornton
8fa7ff647a Defer standard authentication checks to DefaultAuthorizationHandler 2024-05-29 14:35:41 -04:00
Bond-009
d0336cd67e
Merge pull request #11857 from gnattu/fix-ffprobe-useragent
Fix ffprobe -user_agent parameter
2024-05-29 10:00:36 +02:00
Shadowghost
cfab4eb2fc Fix xUnit 2024-05-28 22:08:33 +02:00
Shadowghost
5f1c5009d3 Upgrade dependencies 2024-05-28 22:08:33 +02:00
gnattu
97d7151289 Don't use finally block for tag fallback
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-28 20:24:40 +08:00
gnattu
475fa36ea3
Use better exception logging
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-28 20:23:03 +08:00
gnattu
e8d1ee0934 Use music metadata from ffprobe when TagLib fails
TagLib has its own limitations, which cause it to fail on certain audio files or extract incomplete information from the tags. Use the information from ffprobe when TagLib fails to extract data.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-28 14:24:36 +08:00
gnattu
d07ec4ad0f Fix ffprobe -user_agent parameter
The PR #10448 was doing it wrong and actually did not work. Even its unit testing was testing for a wrong output.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-28 03:18:46 +08:00
Shadowghost
684dfedbcc Do not remove already set people on locked items 2024-05-27 19:14:17 +02:00
Jellyfin Release Bot
730b01fb14 Bump version to 10.9.3
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2024-05-26 20:00:29 -04:00
gnattu
45e8872cc0
Extract media attachment one by one if the filename appears to be a path (#11812) 2024-05-26 11:40:28 -06:00
Shadowghost
cc2c00d764 Respect locked fields when updating children from parent 2024-05-26 17:01:19 +02:00
gnattu
402a5e2c9f Use simpler config value
Only true and false are supported now

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-26 10:45:38 +08:00
Joshua M. Boniface
bcf884ccfa Update GitHub workflows from Master
Backport fixes from:
 - #11780
 - #11779
 - #11769
 - #11733
 - #11515
2024-05-25 11:55:48 -04:00
gnattu
2eece01acc
Filter invalid IPs on external interface matching (#11766) 2024-05-25 09:44:03 -06:00
gnattu
ef985896e2
Use SharedStream for LiveTV more restrictively (#11805) 2024-05-25 09:43:53 -06:00
Nyanmisaka
5e7514243c
Fix the IOSurf error in QSV transcoding (#11830) 2024-05-25 09:00:30 -06:00
gnattu
b9c0fc69e8 Add Env Var to disable second level cache
This is an attempt to track down possible causes of remaining database lockups. Add an environment variable to disable the second-level cache entirely to see if it works better on systems that still experience lockups.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-25 01:12:00 +08:00
NotSaifA
4a344bebc0
Trickplay: kill ffmpeg when task is cancelled (#11790) 2024-05-24 07:50:55 -06:00
Tim Eisele
b2d54b82fa
Improve reliability of HasChanged check (#11792) 2024-05-24 07:50:09 -06:00
gnattu
e7b1162cb3
Force more compatible transcoding profile for LiveTV (#11801) 2024-05-24 07:36:59 -06:00
Tim Eisele
d89e5a0074
Exclude virtual items from DateLastMediaAdded calculation (#11804) 2024-05-24 07:36:39 -06:00
Tim Eisele
4a54e5ddeb
Add Canceled to ended state (#11808) 2024-05-24 07:36:19 -06:00
Bond-009
d9232e05f1
Merge pull request #11798 from gnattu/fix-trickplay-image-height
Recalculate trickplay image height for anamorphic videos
2024-05-24 14:19:37 +02:00
Bond-009
52be8be28f
Merge pull request #11754 from Shadowghost/fix-bd-chapter-images
Fix BD/DVD folder chapter image extraction
2024-05-24 14:18:10 +02:00
Bond-009
ab6c2424db
Merge pull request #11802 from crobibero/nullable-match-term
Mark SearchHint.MatchedTerm as nullable
2024-05-24 14:17:59 +02:00
Bond-009
eb437e7163
Merge pull request #11799 from nyanmisaka/fix-va-vk-interop-chk
Disable VA-VK interop on not supported kernel versions
2024-05-24 14:17:51 +02:00
Bond-009
2ddf2a7866
Merge pull request #11781 from Bond-009/orderts
Retain order blu-ray segments
2024-05-24 14:16:45 +02:00
Bond-009
60232ce9be
Merge pull request #11788 from gnattu/override-too-small-trickplay-image-interval
Override too small trickplay image interval
2024-05-24 14:16:33 +02:00
gnattu
952995f796 Ignore NullOrEmpty imageStream.AspectRatio
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:08:29 +08:00
gnattu
933a285bf5 Use single quote
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:03:35 +08:00
gnattu
f8da69f8e5 Allow decimal aspect ratio
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:02:57 +08:00
Cody Robibero
d5e29bfce3 here you go Niels 2024-05-23 07:16:56 -06:00
nyanmisaka
ab36c4c011 Disable VA-VK interop on not supported kernel versions
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-05-23 14:53:00 +08:00
gnattu
c6e29647fc Recalculate trickplay image height for anamorphic videos
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 12:36:43 +08:00
Shadowghost
95a6291c34 Fixes 2024-05-22 19:26:19 +02:00
gnattu
fa2bff30f6
Explicitly use decimal
Co-authored-by: Cody Robibero <cody@robibe.ro>
2024-05-22 21:17:00 +08:00
Shadowghost
58041e1f9d Fix backwards merge 2024-05-22 13:44:45 +02:00
gnattu
447f73caf4 Fix bitrate calculation accuracy
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-22 13:41:54 +08:00
Shadowghost
9145be6bfc Remove local metadata stop logic 2024-05-22 07:38:53 +02:00
gnattu
86129589ef Override too small trickplay image interval
Some users may set this value too low, causing the trickplay generation to fail. Reset this interval to the minimum valid value of 1 second when the user-configured value is too small to prevent generation failures.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-22 13:35:28 +08:00
Shadowghost
f5a8fca22f Don't drop existing metadata if item gets local locked 2024-05-21 23:22:44 +02:00
Shadowghost
2a612611b8 Extend minimum local metadata requirements 2024-05-21 23:14:28 +02:00
Shadowghost
0b64426cf2 Fix local locked and StopRefreshIfLocalMetadataFound logic 2024-05-21 23:08:00 +02:00
Shadowghost
f3bf9bcdc8 Fix final merge logic 2024-05-21 21:47:29 +02:00
Niels van Velzen
06a5ddda5e
Merge pull request #11774 from Bond-009/downmix
Apply audio boost when downmixing regardless of downmixalgo
2024-05-21 21:40:22 +02:00
Shadowghost
8a5a93ee80 Allow removal of all people from an item 2024-05-21 21:14:30 +02:00
Bond_009
6777f47e0e Retain order blu-ray segments 2024-05-21 20:19:15 +02:00
Bond_009
cf04e1d8e5 Apply audio boost when downmixing regardless of downmixalgo
This is what I'd expect from the naming
Should fix #11771
2024-05-21 16:24:43 +02:00
Bond-009
d608f1e3cc
Merge pull request #11713 from gnattu/fix-vt-h264-profile
Fix VideoToolbox H264 constrained profile option
2024-05-21 13:58:01 +02:00
Bond-009
86f5c93434
Merge pull request #11739 from Shadowghost/fix-trickplay
Do not run trickplay on scan if disabled
2024-05-21 13:57:41 +02:00
Bond-009
4fcbeef5e6
Merge pull request #11738 from crobibero/non-user-session
Don't require user when getting current session
2024-05-21 13:57:31 +02:00
Shadowghost
7d983ae0dd Mark Audio as RequiresDeserialization and backfill data 2024-05-20 22:56:22 +02:00
Shadowghost
a2ab34ef4c Rename provider to be run after normal season NFO provider 2024-05-20 20:12:52 +02:00
Shadowghost
77abafca8e Fix BD/DVD folder chapter image extraction 2024-05-20 13:19:05 +02:00
Shadowghost
e67eb48540 Never revert locked state 2024-05-20 12:49:26 +02:00
Shadowghost
37d7e8f5bf Fix replacement logic 2024-05-20 12:24:57 +02:00
Shadowghost
e6eef8bece Set path for season folders 2024-05-20 01:45:12 +02:00
Shadowghost
52cfd9f261 Properly pass replace flag to remote provider logic 2024-05-19 19:59:47 +02:00
Shadowghost
9a9e8e2648 Prevent infite loop 2024-05-19 18:36:18 +02:00
Shadowghost
2cebd5e05f Do not run trickplay on scan if disabled 2024-05-19 16:52:59 +02:00
Shadowghost
7fa72260ca Only set season name if it's unset 2024-05-19 16:50:51 +02:00
Cody Robibero
b19b346670 Don't require user when getting current session 2024-05-19 08:25:38 -06:00
Shadowghost
99de0ca45f Directly use ParentId 2024-05-18 21:28:07 +02:00
Shadowghost
c106b399d7 Apply review suggestion 2024-05-18 20:41:04 +02:00
Shadowghost
c274062e87 Move NFO series season name parsing to own local provider 2024-05-18 20:29:08 +02:00
gnattu
53de8c0805 Fix VideoToolbox H264 constrained profile option
Like a lot of other encoders they need an underscore between two words.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-18 19:53:23 +08:00
Jellyfin Release Bot
76854b5eff Bump version to 10.9.2 2024-05-17 16:09:34 -04:00
Tim Eisele
5200633574
Prevent double iterating over all seasons (#11700) 2024-05-17 12:13:49 -06:00
Joshua M. Boniface
832e27a8fb
Merge pull request #11680 from Shadowghost/secure-local-playlists
Secure local playlist path handling
2024-05-17 13:47:47 -04:00
Joshua M. Boniface
2da06bc0b1
Merge pull request #11647 from Shadowghost/fix-season-names
Fix season names
2024-05-17 13:47:24 -04:00
Joshua M. Boniface
46c748d888
Merge pull request #11699 from cvium/fix_livetv
Use MediaType instead of ToString and add text/ as disallowed mimetypes
2024-05-17 13:47:12 -04:00
Tim Eisele
430d450828
Fix network binding (#11671)
* Fix network binding

* Better log output

* Fix Kestrel bind message
2024-05-17 18:57:18 +02:00
Niels van Velzen
02937873b1
Merge pull request #11689 from gnattu/workaround-ffmpeg-seek
Workaround ffmpeg keyframe seeking for external subtitles
2024-05-17 18:55:01 +02:00
Niels van Velzen
d303ca56e3
Merge pull request #11698 from Bond-009/issue11605
Fix not binding to SQL parameters
2024-05-17 18:54:32 +02:00
cvium
c647143e53 fix(livetv): use MediaType instead of ToString and add text/ as disallowed mimetypes 2024-05-17 18:50:44 +02:00
Bond_009
dd0ab8ed56 Fix not binding to SQL parameters
Whitespace values weren't being filtered out in advance
Remove the posibility of this happening again by always binding

Should fix #11605
2024-05-17 18:35:11 +02:00
Shadowghost
18e6c1ef7d Apply review comments 2024-05-17 10:58:00 +02:00
gnattu
dec2032e13 Workaround ffmpeg keyframe seeking for external subtitles
We seek to the exact position of the keyframe for direct stream/remuxing, but FFmpeg seeks to the previous keyframe when the exact time is provided as input. To work around this, add a 0.5 second offset to the seeking time.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-17 12:12:45 +08:00
Shadowghost
287e06d6dc If itemPath exists, use that, otherwise try getting full path from relative path 2024-05-16 21:55:00 +02:00
Shadowghost
dc93cc13b5 Apply review suggestion 2024-05-16 19:36:49 +02:00
Niels van Velzen
26714e2c62
Merge pull request #11673 from Shadowghost/fix-local-playlists
Fix local playlist scanning
2024-05-16 19:07:17 +02:00
Tim Eisele
c6c48a2b47
Fix series status parsing (#11648)
* Fix series status parsing

* Apply review suggestions and add test

* Apply review suggestion

* Apply review suggestions
2024-05-16 19:06:11 +02:00
Niels van Velzen
f8b67ec44c
Merge pull request #11670 from jellyfin/attempt-restore-user-cache
Restore caching for UserManager
2024-05-16 19:05:00 +02:00
Niels van Velzen
ddd5c302b4
Merge pull request #11675 from gnattu/fix-vaapi-mjpeg-parameter
Fix quality parameter for vaapi_mjpeg
2024-05-16 19:04:34 +02:00
Niels van Velzen
9b98638b2b
Merge pull request #11677 from Bond-009/migrateuserdb
Properly dispose dbContext in MigrateUserDb
2024-05-16 19:02:52 +02:00
Shadowghost
2ca8ce6f60 Apply review suggestions 2024-05-16 17:04:42 +02:00
Shadowghost
56a158e5c9 Secure local playlist path handling 2024-05-16 16:10:37 +02:00
gnattu
9b65d243a8 Use OrdinalIgnoreCase comparison
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-16 20:18:36 +08:00
Bond_009
c45dd5d6fb Properly dispose dbContext in MigrateUserDb 2024-05-16 10:58:32 +02:00
Shadowghost
af4b732080 Fix override of locked name 2024-05-16 08:25:35 +02:00
Shadowghost
1cdf0f5cc4 Apply review suggestion 2024-05-16 08:22:40 +02:00
gnattu
20a1da1855 fix quality parameter for vaapi
Hardware encoders has different expectations about quality input. VAAPI's mjpeg encoder excepts JPEG quality divided by QP2LAMBDA as input.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-16 11:49:41 +08:00
gnattu
8aee50020b
Always fallback for failed HEAD request (#11668) 2024-05-15 17:27:25 -06:00
Bond-009
c1615419b9
Don't generate TrickPlay images for files that don't exist (#11653) 2024-05-15 17:26:42 -06:00
Shadowghost
15489eeae3 Fix album photos 2024-05-16 00:31:50 +02:00
Shadowghost
80c9589885 Fix local playlist scanning 2024-05-15 23:20:14 +02:00
Cody Robibero
a5d60c4521
Allow empty user id when getting device list (#11633) 2024-05-15 07:06:29 -06:00
Bill Thornton
2cb052a119
Fix FirstTimeSetupPolicy allowing guest access (#11651) 2024-05-15 07:06:10 -06:00
gnattu
0756174b13 Restore caching for UserManager.cs
It seems like the EFCore's second level cache does not really work, and we are having very heavy database query here.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-15 12:43:46 +08:00
gnattu
3f760e6685
Fix missing filename for timer (#11629) 2024-05-14 21:28:39 -06:00
Nathan McCrina
d5dc4435d9
Handle exception for unexpected audio file YEAR tag values (#11621) 2024-05-14 21:28:29 -06:00
Shadowghost
48228430c0 Fix season names 2024-05-15 01:23:23 +02:00
gnattu
f396a95f05
Fix network config (#11587) 2024-05-13 07:20:47 -06:00
Jellyfin Release Bot
717afcdc82 Bump version to 10.9.1 2024-05-12 20:10:24 -04:00
Tim Gels
25c50bcc5d
Change "try" to "attempt" english translation (#11578) 2024-05-12 15:19:02 -06:00
Cody Robibero
f77a5d0c5c
Default to processor count concurrent scan instead of 2 * processor count (#11569) 2024-05-12 15:18:56 -06:00
gnattu
6689d837d6
Fix absolute path checking on windows (#11570) 2024-05-12 15:12:07 -06:00
Cody Robibero
c1907354e8
Add metrics collector to disposable parts (#11539) 2024-05-12 09:14:36 -06:00
Cody Robibero
efba619acb
Fix migration with special Rating (#11541) 2024-05-12 09:14:26 -06:00
Cody Robibero
7d271547c6 Disable nuget warning in Jellyfin.Extensions 2024-05-11 15:12:14 -06:00
Jellyfin Release Bot
327f92bb2e Bump version to 10.9.0 2024-05-11 14:23:58 -04:00
152 changed files with 2831 additions and 1424 deletions

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"
] ]

View File

@ -20,18 +20,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6

View File

@ -3,6 +3,8 @@ on:
push: push:
branches: branches:
- master - master
tags:
- 'v*'
pull_request_target: pull_request_target:
permissions: {} permissions: {}
@ -14,7 +16,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -39,7 +41,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -99,11 +101,24 @@ jobs:
- id: read-diff - id: read-diff
name: Read openapi-diff output name: Read openapi-diff output
run: | run: |
# Read and fix markdown
body=$(cat openapi-changes.md) body=$(cat openapi-changes.md)
body="${body//'%'/'%25'}" # Write to workflow summary
body="${body//$'\n'/'%0A'}" echo "$body" >> $GITHUB_STEP_SUMMARY
body="${body//$'\r'/'%0D'}" # Set ApiChanged var
echo ::set-output name=body::$body if [ "$body" != '' ]; then
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
else
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
fi
# Add header/footer for diff comment
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
echo "<details>" >> openapi-changes-reply.md
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "$body" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "</details>" >> openapi-changes-reply.md
- name: Find difference comment - name: Find difference comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment id: find-comment
@ -113,22 +128,15 @@ jobs:
body-includes: openapi-diff-workflow-comment body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed) - name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body != '' }} if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }} comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace edit-mode: replace
body: | body-path: openapi-changes-reply.md
<!--openapi-diff-workflow-comment-->
<details>
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
${{ steps.read-diff.outputs.body }}
</details>
- name: Edit difference comment (unchanged) - name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }} comment-id: ${{ steps.find-comment.outputs.comment-id }}
@ -138,11 +146,9 @@ jobs:
No changes to OpenAPI specification found. See history of this comment for previous changes. No changes to OpenAPI specification found. See history of this comment for previous changes.
publish: publish-unstable:
name: OpenAPI - Publish Unstable Spec name: OpenAPI - Publish Unstable Spec
if: | if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
github.event_name != 'pull_request_target' &&
contains(github.repository_owner, 'jellyfin')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- openapi-head - openapi-head
@ -201,3 +207,65 @@ jobs:
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi fi
) 200>/run/workflows/openapi-unstable.lock ) 200>/run/workflows/openapi-unstable.lock
publish-stable:
name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
steps:
- name: Set version number
id: version
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script_stop: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-stable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Create new jellyfin-openapi-stable.json symlink
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
# Check that the previous openapi stable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
fi
) 200>/run/workflows/openapi-stable.lock

View File

@ -19,7 +19,7 @@ jobs:
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
steps: steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 - uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with: with:
@ -34,7 +34,7 @@ jobs:
--verbosity minimal --verbosity minimal
- name: Merge code coverage results - name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@2a2d60ea1c7e811f54684179af6ac1ae8c1ce69a # 5.2.5 uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
with: with:
reports: "**/coverage.cobertura.xml" reports: "**/coverage.cobertura.xml"
targetdir: "merged/" targetdir: "merged/"

View File

@ -24,7 +24,7 @@ jobs:
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@ -51,7 +51,7 @@ jobs:
reactions: eyes reactions: eyes
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@ -128,7 +128,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python

View File

@ -10,7 +10,7 @@ jobs:
issues: write issues: write
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python

View File

@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }} if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps: steps:
- name: Apply label - name: Apply label
uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0 uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with: with:
dirtyLabel: 'merge conflict' dirtyLabel: 'merge conflict'

View File

@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8 yq-version: v4.9.8
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}
@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}

2
.gitignore vendored
View File

@ -277,3 +277,5 @@ apiclient/generated
# Omnisharp crash logs # Omnisharp crash logs
mono_crash.*.json mono_crash.*.json
out

View File

@ -183,6 +183,7 @@
- [btopherjohnson](https://github.com/btopherjohnson) - [btopherjohnson](https://github.com/btopherjohnson)
- [GeorgeH005](https://github.com/GeorgeH005) - [GeorgeH005](https://github.com/GeorgeH005)
- [Vedant](https://github.com/viktory36/) - [Vedant](https://github.com/viktory36/)
- [NotSaifA](https://github.com/NotSaifA)
# Emby Contributors # Emby Contributors
@ -255,3 +256,4 @@
- [JPUC1143](https://github.com/Jpuc1143/) - [JPUC1143](https://github.com/Jpuc1143/)
- [0x25CBFC4F](https://github.com/0x25CBFC4F) - [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [Robert Lützner](https://github.com/rluetzner) - [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina)

View File

@ -16,40 +16,39 @@
<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.4.3" />
<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.4" /> <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.4" /> <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.4" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" /> <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.4" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.4" /> <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" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
@ -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" />
@ -74,19 +73,19 @@
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" /> <PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.6.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<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" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.7.1" /> <PackageVersion Include="xunit" Version="2.8.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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.0</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>

View File

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

View File

@ -0,0 +1,44 @@
using System;
using System.Linq;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.TV;
/// <summary>
/// Helper class for TV metadata parsing.
/// </summary>
public static class TvParserHelpers
{
private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
private static readonly string[] _endedState = ["Cancelled", "Canceled"];
/// <summary>
/// Tries to parse a string into <see cref="SeriesStatus"/>.
/// </summary>
/// <param name="status">The status string.</param>
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
/// <returns>Returns true if parsing was successful.</returns>
public static bool TryParseSeriesStatus(string status, out SeriesStatus? enumValue)
{
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
{
enumValue = seriesStatus;
return true;
}
if (_continuingState.Contains(status, StringComparer.OrdinalIgnoreCase))
{
enumValue = SeriesStatus.Continuing;
return true;
}
if (_endedState.Contains(status, StringComparer.OrdinalIgnoreCase))
{
enumValue = SeriesStatus.Ended;
return true;
}
enumValue = null;
return false;
}
}

View File

@ -422,7 +422,7 @@ namespace Emby.Server.Implementations
// Initialize runtime stat collection // Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics) if (ConfigurationManager.Configuration.EnableMetrics)
{ {
DotNetRuntimeStatsBuilder.Default().StartCollecting(); _disposableParts.Add(DotNetRuntimeStatsBuilder.Default().StartCollecting());
} }
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();

View File

@ -119,5 +119,56 @@ namespace Emby.Server.Implementations.Configuration
EnsureWriteAccess(newPath); EnsureWriteAccess(newPath);
} }
} }
/// <summary>
/// Logs a message.
/// </summary>
/// <param name="message">The message to log.</param>
public void LogMessage(string message)
{
Logger.LogInformation("(MJH) {Message}", message);
}
private bool IsSubPath(string basePath, string testPath)
{
string normalizedBasePath = Path.GetFullPath(basePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string normalizedTestPath = Path.GetFullPath(testPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var isSubPath = normalizedTestPath.StartsWith(normalizedBasePath, StringComparison.OrdinalIgnoreCase)
&& normalizedTestPath.Length > normalizedBasePath.Length
&& (normalizedTestPath[normalizedBasePath.Length] == Path.DirectorySeparatorChar
|| normalizedTestPath[normalizedBasePath.Length] == Path.AltDirectorySeparatorChar);
return isSubPath;
}
/// <summary>
/// Fetches the folder priority.
/// </summary>
/// <param name="path">The path to test.</param>
/// <returns>The priority int value (higher comes first).</returns>
public int FetchFolderPriority(string path)
{
if (this.IsSubPath("/mnt/media-pool", path))
{
return 4;
}
if (this.IsSubPath("/mnt/lee/laptop-new", path))
{
return 3;
}
if (this.IsSubPath("/mnt/lee/laptop-seagate", path))
{
return 2;
}
if (this.IsSubPath("/mnt/lee/pi-elements", path))
{
return 1;
}
return 0;
}
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
public abstract class BaseSqliteRepository : IDisposable public abstract class BaseSqliteRepository : IDisposable
{ {
private bool _disposed = false; private bool _disposed = false;
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
private SqliteConnection _writeConnection;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class. /// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
@ -98,9 +101,55 @@ namespace Emby.Server.Implementations.Data
} }
} }
protected SqliteConnection GetConnection() protected ManagedConnection GetConnection(bool readOnly = false)
{ {
var connection = new SqliteConnection($"Filename={DbFilePath}"); if (!readOnly)
{
_writeLock.Wait();
if (_writeConnection is not null)
{
return new ManagedConnection(_writeConnection, _writeLock);
}
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
writeConnection.Open();
if (CacheSize.HasValue)
{
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
}
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
connection.Open(); connection.Open();
if (CacheSize.HasValue) if (CacheSize.HasValue)
@ -135,17 +184,17 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA temp_store=" + (int)TempStore); connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection; return new ManagedConnection(connection, null);
} }
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql) public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
{ {
var command = connection.CreateCommand(); var command = connection.CreateCommand();
command.CommandText = sql; command.CommandText = sql;
return command; return command;
} }
protected bool TableExists(SqliteConnection connection, string name) protected bool TableExists(ManagedConnection connection, string name)
{ {
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master"); using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
@ -159,7 +208,7 @@ namespace Emby.Server.Implementations.Data
return false; return false;
} }
protected List<string> GetColumnNames(SqliteConnection connection, string table) protected List<string> GetColumnNames(ManagedConnection connection, string table)
{ {
var columnNames = new List<string>(); var columnNames = new List<string>();
@ -174,7 +223,7 @@ namespace Emby.Server.Implementations.Data
return columnNames; return columnNames;
} }
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames) protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{ {
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase)) if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{ {
@ -207,6 +256,24 @@ namespace Emby.Server.Implementations.Data
return; return;
} }
if (dispose)
{
_writeLock.Wait();
try
{
_writeConnection.Dispose();
}
finally
{
_writeLock.Release();
}
_writeLock.Dispose();
}
_writeConnection = null;
_writeLock = null;
_disposed = true; _disposed = true;
} }
} }

View File

@ -0,0 +1,62 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data;
public sealed class ManagedConnection : IDisposable
{
private readonly SemaphoreSlim? _writeLock;
private SqliteConnection _db;
private bool _disposed = false;
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
{
_db = db;
_writeLock = writeLock;
}
public SqliteTransaction BeginTransaction()
=> _db.BeginTransaction();
public SqliteCommand CreateCommand()
=> _db.CreateCommand();
public void Execute(string commandText)
=> _db.Execute(commandText);
public SqliteCommand PrepareStatement(string sql)
=> _db.PrepareStatement(sql);
public IEnumerable<SqliteDataReader> Query(string commandText)
=> _db.Query(commandText);
public void Dispose()
{
if (_disposed)
{
return;
}
if (_writeLock is null)
{
// Read connections are managed with an internal pool
_db.Dispose();
}
else
{
// Write lock is managed by BaseSqliteRepository
// Don't dispose here
_writeLock.Release();
}
_db = null!;
_disposed = true;
}
}

View File

@ -601,7 +601,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit(); transaction.Commit();
} }
private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
{ {
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
@ -1261,7 +1261,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed(); CheckDisposed();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{ {
statement.TryBind("@guid", id); statement.TryBind("@guid", id);
@ -1298,16 +1298,15 @@ namespace Emby.Server.Implementations.Data
&& type != typeof(Book) && type != typeof(Book)
&& type != typeof(LiveTvProgram) && type != typeof(LiveTvProgram)
&& type != typeof(AudioBook) && type != typeof(AudioBook)
&& type != typeof(Audio)
&& type != typeof(MusicAlbum); && type != typeof(MusicAlbum);
} }
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query) private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
{ {
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query)); return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false);
} }
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization)
{ {
var typeString = reader.GetString(0); var typeString = reader.GetString(0);
@ -1320,7 +1319,7 @@ namespace Emby.Server.Implementations.Data
BaseItem item = null; BaseItem item = null;
if (TypeRequiresDeserialization(type)) if (TypeRequiresDeserialization(type) && !skipDeserialization)
{ {
try try
{ {
@ -1888,7 +1887,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed(); CheckDisposed();
var chapters = new List<ChapterInfo>(); var chapters = new List<ChapterInfo>();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
{ {
statement.TryBind("@ItemId", item.Id); statement.TryBind("@ItemId", item.Id);
@ -1907,7 +1906,7 @@ namespace Emby.Server.Implementations.Data
{ {
CheckDisposed(); CheckDisposed();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{ {
statement.TryBind("@ItemId", item.Id); statement.TryBind("@ItemId", item.Id);
@ -1981,7 +1980,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit(); transaction.Commit();
} }
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db) private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
{ {
var startIndex = 0; var startIndex = 0;
var limit = 100; var limit = 100;
@ -2323,7 +2322,7 @@ namespace Emby.Server.Implementations.Data
columns.Add(builder.ToString()); columns.Add(builder.ToString());
query.ExcludeItemIds = [..query.ExcludeItemIds, item.Id, ..item.ExtraIds]; query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds];
query.ExcludeProviderIds = item.ProviderIds; query.ExcludeProviderIds = item.ProviderIds;
} }
@ -2470,7 +2469,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString(); var commandText = commandTextBuilder.ToString();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
@ -2538,7 +2537,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString(); var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>(); var items = new List<BaseItem>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
@ -2562,7 +2561,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
{ {
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization);
if (item is not null) if (item is not null)
{ {
items.Add(item); items.Add(item);
@ -2746,7 +2745,7 @@ namespace Emby.Server.Implementations.Data
var list = new List<BaseItem>(); var list = new List<BaseItem>();
var result = new QueryResult<BaseItem>(); var result = new QueryResult<BaseItem>();
using var connection = GetConnection(); using var connection = GetConnection(true);
using var transaction = connection.BeginTransaction(); using var transaction = connection.BeginTransaction();
if (!isReturningZeroItems) if (!isReturningZeroItems)
{ {
@ -2774,7 +2773,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
{ {
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
if (item is not null) if (item is not null)
{ {
list.Add(item); list.Add(item);
@ -2831,7 +2830,7 @@ namespace Emby.Server.Implementations.Data
prepend.Add((ItemSortBy.Random, SortOrder.Ascending)); prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
} }
orderBy = query.OrderBy = [..prepend, ..orderBy]; orderBy = query.OrderBy = [.. prepend, .. orderBy];
} }
else if (orderBy.Count == 0) else if (orderBy.Count == 0)
{ {
@ -2928,7 +2927,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString(); var commandText = commandTextBuilder.ToString();
var list = new List<Guid>(); var list = new List<Guid>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
@ -4477,7 +4476,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
transaction.Commit(); transaction.Commit();
} }
private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value) private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
{ {
using (var statement = PrepareStatement(db, query)) using (var statement = PrepareStatement(db, query))
{ {
@ -4510,7 +4509,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
} }
var list = new List<string>(); var list = new List<string>();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString())) using (var statement = PrepareStatement(connection, commandText.ToString()))
{ {
// Run this again to bind the params // Run this again to bind the params
@ -4548,7 +4547,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
} }
var list = new List<PersonInfo>(); var list = new List<PersonInfo>();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString())) using (var statement = PrepareStatement(connection, commandText.ToString()))
{ {
// Run this again to bind the params // Run this again to bind the params
@ -4633,7 +4632,7 @@ AND Type = @InternalPersonType)");
return whereClauses; return whereClauses;
} }
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement) private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
{ {
if (itemId.IsEmpty()) if (itemId.IsEmpty())
{ {
@ -4788,7 +4787,7 @@ AND Type = @InternalPersonType)");
var list = new List<string>(); var list = new List<string>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
@ -4988,8 +4987,8 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>(); var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>(); var result = new QueryResult<(BaseItem, ItemCounts)>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var transaction = connection.BeginTransaction(deferred: true)) using (var transaction = connection.BeginTransaction())
{ {
if (!isReturningZeroItems) if (!isReturningZeroItems)
{ {
@ -5021,7 +5020,7 @@ AND Type = @InternalPersonType)");
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
{ {
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
if (item is not null) if (item is not null)
{ {
var countStartColumn = columns.Count - 1; var countStartColumn = columns.Count - 1;
@ -5144,12 +5143,12 @@ AND Type = @InternalPersonType)");
list.AddRange(inheritedTags.Select(i => (6, i))); list.AddRange(inheritedTags.Select(i => (6, i)));
// Remove all invalid values. // Remove all invalid values.
list.RemoveAll(i => string.IsNullOrEmpty(i.Item2)); list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
return list; return list;
} }
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db) private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
{ {
if (itemId.IsEmpty()) if (itemId.IsEmpty())
{ {
@ -5168,7 +5167,7 @@ AND Type = @InternalPersonType)");
InsertItemValues(itemId, values, db); InsertItemValues(itemId, values, db);
} }
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db) private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
{ {
const int Limit = 100; const int Limit = 100;
var startIndex = 0; var startIndex = 0;
@ -5202,12 +5201,6 @@ AND Type = @InternalPersonType)");
var itemValue = currentValueInfo.Value; var itemValue = currentValueInfo.Value;
// Don't save if invalid
if (string.IsNullOrWhiteSpace(itemValue))
{
continue;
}
statement.TryBind("@Type" + index, currentValueInfo.MagicNumber); statement.TryBind("@Type" + index, currentValueInfo.MagicNumber);
statement.TryBind("@Value" + index, itemValue); statement.TryBind("@Value" + index, itemValue);
statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue)); statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
@ -5228,24 +5221,25 @@ AND Type = @InternalPersonType)");
throw new ArgumentNullException(nameof(itemId)); throw new ArgumentNullException(nameof(itemId));
} }
ArgumentNullException.ThrowIfNull(people);
CheckDisposed(); CheckDisposed();
using var connection = GetConnection(); using var connection = GetConnection();
using var transaction = connection.BeginTransaction(); using var transaction = connection.BeginTransaction();
// First delete chapters // Delete all existing people first
using var command = connection.CreateCommand(); using var command = connection.CreateCommand();
command.CommandText = "delete from People where ItemId=@ItemId"; command.CommandText = "delete from People where ItemId=@ItemId";
command.TryBind("@ItemId", itemId); command.TryBind("@ItemId", itemId);
command.ExecuteNonQuery(); command.ExecuteNonQuery();
if (people is not null)
{
InsertPeople(itemId, people, connection); InsertPeople(itemId, people, connection);
}
transaction.Commit(); transaction.Commit();
} }
private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db) private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
{ {
const int Limit = 100; const int Limit = 100;
var startIndex = 0; var startIndex = 0;
@ -5341,7 +5335,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by StreamIndex ASC"; cmdText += " order by StreamIndex ASC";
using (var connection = GetConnection()) using (var connection = GetConnection(true))
{ {
var list = new List<MediaStream>(); var list = new List<MediaStream>();
@ -5394,7 +5388,7 @@ AND Type = @InternalPersonType)");
transaction.Commit(); transaction.Commit();
} }
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db) private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
{ {
const int Limit = 10; const int Limit = 10;
var startIndex = 0; var startIndex = 0;
@ -5700,14 +5694,18 @@ 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.LocalizedDefault = _localization.GetLocalizedString("Default");
item.LocalizedExternal = _localization.GetLocalizedString("External");
if (item.Type is MediaStreamType.Subtitle)
{ {
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
item.LocalizedDefault = _localization.GetLocalizedString("Default");
item.LocalizedForced = _localization.GetLocalizedString("Forced"); item.LocalizedForced = _localization.GetLocalizedString("Forced");
item.LocalizedExternal = _localization.GetLocalizedString("External");
item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
} }
}
return item; return item;
} }
@ -5728,7 +5726,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by AttachmentIndex ASC"; cmdText += " order by AttachmentIndex ASC";
var list = new List<MediaAttachment>(); var list = new List<MediaAttachment>();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, cmdText)) using (var statement = PrepareStatement(connection, cmdText))
{ {
statement.TryBind("@ItemId", query.ItemId); statement.TryBind("@ItemId", query.ItemId);
@ -5778,7 +5776,7 @@ AND Type = @InternalPersonType)");
private void InsertMediaAttachments( private void InsertMediaAttachments(
Guid id, Guid id,
IReadOnlyList<MediaAttachment> attachments, IReadOnlyList<MediaAttachment> attachments,
SqliteConnection db, ManagedConnection db,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
const int InsertAtOnce = 10; const int InsertAtOnce = 10;

View File

@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
} }
} }
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users) private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
{ {
var userIdsWithUserData = GetAllUserIdsWithUserData(db); var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Data
} }
} }
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db) private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
{ {
var list = new List<Guid>(); var list = new List<Guid>();
@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Data
} }
} }
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData) private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
{ {
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{ {
@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key); ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection()) using (var connection = GetConnection(true))
{ {
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{ {

View File

@ -80,12 +80,14 @@ namespace Emby.Server.Implementations.IO
public virtual string MakeAbsolutePath(string folderPath, string filePath) public virtual string MakeAbsolutePath(string folderPath, string filePath)
{ {
// path is actually a stream // path is actually a stream
if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal)) if (string.IsNullOrWhiteSpace(filePath))
{ {
return filePath; return filePath;
} }
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') var isAbsolutePath = Path.IsPathRooted(filePath) && (!OperatingSystem.IsWindows() || filePath[0] != '\\');
if (isAbsolutePath)
{ {
// absolute local path // absolute local path
return filePath; return filePath;
@ -97,17 +99,10 @@ namespace Emby.Server.Implementations.IO
return filePath; return filePath;
} }
var firstChar = filePath[0];
if (firstChar == '/')
{
// for this we don't really know
return filePath;
}
var filePathSpan = filePath.AsSpan(); var filePathSpan = filePath.AsSpan();
// relative path // relative path on windows
if (firstChar == '\\') if (filePath[0] == '\\')
{ {
filePathSpan = filePathSpan.Slice(1); filePathSpan = filePathSpan.Slice(1);
} }
@ -394,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)
{ {
@ -422,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;
} }

View File

@ -11,7 +11,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images namespace Emby.Server.Implementations.Images
{ {
@ -33,12 +32,12 @@ namespace Emby.Server.Implementations.Images
Parent = item, Parent = item,
Recursive = true, Recursive = true,
DtoOptions = new DtoOptions(true), DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary }, ImageTypes = [ImageType.Primary],
OrderBy = new (ItemSortBy, SortOrder)[] OrderBy =
{ [
(ItemSortBy.IsFolder, SortOrder.Ascending), (ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending) (ItemSortBy.SortName, SortOrder.Ascending)
}, ],
Limit = 1 Limit = 1
}); });
} }

View File

@ -1,7 +1,10 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -15,5 +18,13 @@ namespace Emby.Server.Implementations.Images
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{ {
} }
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
var items = base.GetItemsWithImages(item);
// Ignore any folders because they can have generated collages
return items.Where(i => i is not Folder).ToList();
}
} }
} }

View File

@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{ {
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
@ -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)
{ {
@ -2812,9 +2812,11 @@ namespace Emby.Server.Implementations.Library
} }
_itemRepository.UpdatePeople(item.Id, people); _itemRepository.UpdatePeople(item.Id, people);
if (people is not null)
{
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
} }
}
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure) public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure)
{ {
@ -3207,5 +3209,10 @@ namespace Emby.Server.Implementations.Library
return item is UserRootFolder || item.IsVisibleStandalone(user); return item is UserRootFolder || item.IsVisibleStandalone(user);
} }
public void LogMessage(string message)
{
_logger.LogInformation("(MJH) {Message}", message);
}
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common; using Emby.Naming.Common;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
@ -85,6 +86,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
} }
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService); var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
var albumParser = new AlbumParser(_namingOptions);
var directories = args.FileSystemChildren.Where(i => i.IsDirectory); var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
@ -100,6 +102,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
} }
} }
// If the folder is a multi-disc folder, then it is not an artist folder
if (albumParser.IsMultiPart(fileSystemInfo.FullName))
{
return;
}
// If we contain a music album assume we are an artist folder // If we contain a music album assume we are an artist folder
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService)) if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
{ {

View File

@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
IndexNumber = seasonParserResult.SeasonNumber, IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id, SeriesId = series.Id,
SeriesName = series.Name SeriesName = series.Name,
Path = seasonParserResult.IsSeasonFolder ? path : null
}; };
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder) if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
@ -78,18 +79,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
} }
} }
if (season.IndexNumber.HasValue) if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
{ {
var seasonNumber = season.IndexNumber.Value; var seasonNumber = season.IndexNumber.Value;
if (string.IsNullOrEmpty(season.Name))
{
var seasonNames = series.SeasonNames;
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
{
season.Name = seasonName;
}
else
{
season.Name = seasonNumber == 0 ? season.Name = seasonNumber == 0 ?
args.LibraryOptions.SeasonZeroDisplayName : args.LibraryOptions.SeasonZeroDisplayName :
string.Format( string.Format(
@ -98,8 +90,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
seasonNumber, seasonNumber,
args.LibraryOptions.PreferredMetadataLanguage); args.LibraryOptions.PreferredMetadataLanguage);
} }
}
}
return season; return season;
} }

View File

@ -13,7 +13,7 @@
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} is connected",
"External": "External", "External": "External",
"FailedLoginAttemptWithUserName": "Failed login try from {0}", "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites", "Favorites": "Favorites",
"Folders": "Folders", "Folders": "Folders",
"Forced": "Forced", "Forced": "Forced",

View File

@ -321,7 +321,11 @@ namespace Emby.Server.Implementations.Localization
// Try splitting by : to handle "Germany: FSK-18" // Try splitting by : to handle "Germany: FSK-18"
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase)) if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
{ {
return GetRatingLevel(rating.AsSpan().RightPart(':').ToString()); var ratingLevelRightPart = rating.AsSpan().RightPart(':');
if (ratingLevelRightPart.Length != 0)
{
return GetRatingLevel(ratingLevelRightPart.ToString());
}
} }
// Handle prefix country code to handle "DE-18" // Handle prefix country code to handle "DE-18"
@ -332,8 +336,12 @@ namespace Emby.Server.Implementations.Localization
// Extract culture from country prefix // Extract culture from country prefix
var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString()); var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
var ratingLevelRightPart = ratingSpan.RightPart('-');
if (ratingLevelRightPart.Length != 0)
{
// Check rating system of culture // Check rating system of culture
return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName); return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
}
} }
return null; return null;

View File

@ -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

View File

@ -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

View File

@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
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.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
@ -69,7 +70,7 @@ public partial class AudioNormalizationTask : IScheduledTask
/// <inheritdoc /> /// <inheritdoc />
public string Key => "AudioNormalization"; public string Key => "AudioNormalization";
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")] [GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
private static partial Regex LUFSRegex(); private static partial Regex LUFSRegex();
/// <inheritdoc /> /// <inheritdoc />
@ -105,14 +106,21 @@ 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);
try
{
a.LUFS = await CalculateLUFSAsync( a.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
}
finally
{
File.Delete(tempFile); File.Delete(tempFile);
} }
}
_itemRepository.SaveItems(albums, cancellationToken); _itemRepository.SaveItems(albums, cancellationToken);
@ -179,16 +187,17 @@ public partial class AudioNormalizationTask : IScheduledTask
} }
using var reader = process.StandardError; using var reader = process.StandardError;
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
cancellationToken.ThrowIfCancellationRequested();
MatchCollection split = LUFSRegex().Matches(output);
if (split.Count != 0)
{ {
return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); Match match = LUFSRegex().Match(line);
if (match.Success)
{
return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
}
} }
_logger.LogError("Failed to find LUFS value in output:\n{Output}", output); _logger.LogError("Failed to find LUFS value in output");
return null; return null;
} }
} }

View File

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

View File

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

View File

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

View File

@ -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;
@ -1202,7 +1202,8 @@ namespace Emby.Server.Implementations.Session
new DtoOptions(false) new DtoOptions(false)
{ {
EnableImages = false EnableImages = false
}) },
user.DisplayMissingEpisodes)
.Where(i => !i.IsVirtualItem) .Where(i => !i.IsVirtualItem)
.SkipWhile(i => !i.Id.Equals(episode.Id)) .SkipWhile(i => !i.Id.Equals(episode.Id))
.ToList(); .ToList();
@ -1296,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);
@ -1306,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 />
@ -1519,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)
{ {
@ -1552,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)
{ {
@ -1596,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)
{ {
@ -1786,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)
{ {

View File

@ -1,5 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -24,20 +25,31 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
/// <inheritdoc /> /// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{ {
// Succeed if the startup wizard / first time setup is not complete
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{ {
context.Succeed(requirement); context.Succeed(requirement);
} }
else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
// Succeed if user is admin
else if (context.User.IsInRole(UserRoles.Administrator))
{ {
context.Fail();
}
else
{
// Any user-specific checks are handled in the DefaultAuthorizationHandler.
context.Succeed(requirement); context.Succeed(requirement);
} }
// Fail if admin is required and user is not admin
else if (requirement.RequireAdmin)
{
context.Fail();
}
// Succeed if admin is not required and user is not guest
else if (context.User.IsInRole(UserRoles.User))
{
context.Succeed(requirement);
}
// Any user-specific checks are handled in the DefaultAuthorizationHandler.
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -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)
{ {

View File

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

View File

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

View File

@ -289,18 +289,36 @@ public class ItemUpdateController : BaseJellyfinApiController
if (item is Series rseries) if (item is Series rseries)
{ {
foreach (var season in rseries.Children.OfType<Season>()) foreach (var season in rseries.Children.OfType<Season>())
{
if (!season.LockedFields.Contains(MetadataField.OfficialRating))
{ {
season.OfficialRating = request.OfficialRating; season.OfficialRating = request.OfficialRating;
}
season.CustomRating = request.CustomRating; season.CustomRating = request.CustomRating;
if (!season.LockedFields.Contains(MetadataField.Tags))
{
season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
season.OnMetadataChanged(); season.OnMetadataChanged();
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
foreach (var ep in season.Children.OfType<Episode>()) foreach (var ep in season.Children.OfType<Episode>())
{
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
{ {
ep.OfficialRating = request.OfficialRating; ep.OfficialRating = request.OfficialRating;
}
ep.CustomRating = request.CustomRating; ep.CustomRating = request.CustomRating;
if (!ep.LockedFields.Contains(MetadataField.Tags))
{
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
ep.OnMetadataChanged(); ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
} }
@ -309,10 +327,19 @@ public class ItemUpdateController : BaseJellyfinApiController
else if (item is Season season) else if (item is Season season)
{ {
foreach (var ep in season.Children.OfType<Episode>()) foreach (var ep in season.Children.OfType<Episode>())
{
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
{ {
ep.OfficialRating = request.OfficialRating; ep.OfficialRating = request.OfficialRating;
}
ep.CustomRating = request.CustomRating; ep.CustomRating = request.CustomRating;
if (!ep.LockedFields.Contains(MetadataField.Tags))
{
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
ep.OnMetadataChanged(); ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
} }
@ -320,10 +347,19 @@ public class ItemUpdateController : BaseJellyfinApiController
else if (item is MusicAlbum album) else if (item is MusicAlbum album)
{ {
foreach (BaseItem track in album.Children) foreach (BaseItem track in album.Children)
{
if (!track.LockedFields.Contains(MetadataField.OfficialRating))
{ {
track.OfficialRating = request.OfficialRating; track.OfficialRating = request.OfficialRating;
}
track.CustomRating = request.CustomRating; track.CustomRating = request.CustomRating;
if (!track.LockedFields.Contains(MetadataField.Tags))
{
track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
track.OnMetadataChanged(); track.OnMetadataChanged();
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
} }

View File

@ -180,8 +180,22 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it // No need to start if scanning the library because it will handle it
if (refreshLibrary) if (refreshLibrary)
{ {
await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
if (newLib is CollectionFolder folder)
{
foreach (var child in folder.GetPhysicalFolders())
{
await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
}
else
{
// We don't know if this one can be validated individually, trigger a new validation
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false); await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
} }
}
else else
{ {
// Need to add a delay here or directory watchers may still pick up the changes // Need to add a delay here or directory watchers may still pick up the changes
@ -319,7 +333,7 @@ public class LibraryStructureController : BaseJellyfinApiController
public ActionResult UpdateLibraryOptions( public ActionResult UpdateLibraryOptions(
[FromBody] UpdateLibraryOptionsDto request) [FromBody] UpdateLibraryOptionsDto request)
{ {
var item = _libraryManager.GetItemById<CollectionFolder>(request.Id, User.GetUserId()); var item = _libraryManager.GetItemById<CollectionFolder>(request.Id);
if (item is null) if (item is null)
{ {
return NotFound(); return NotFound();

View File

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

View File

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

View File

@ -231,6 +231,7 @@ public class TvShowsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{ {
@ -240,7 +241,7 @@ public class TvShowsController : BaseJellyfinApiController
return NotFound("No season exists with Id " + seasonId); return NotFound("No season exists with Id " + seasonId);
} }
episodes = seasonItem.GetEpisodes(user, dtoOptions); episodes = seasonItem.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
} }
else if (season.HasValue) // Season number was supplied. Get episodes by season number else if (season.HasValue) // Season number was supplied. Get episodes by season number
{ {
@ -256,7 +257,7 @@ public class TvShowsController : BaseJellyfinApiController
episodes = seasonItem is null ? episodes = seasonItem is null ?
new List<BaseItem>() new List<BaseItem>()
: ((Season)seasonItem).GetEpisodes(user, dtoOptions); : ((Season)seasonItem).GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
} }
else // No season number or season id was supplied. Returning all episodes. else // No season number or season id was supplied. Returning all episodes.
{ {
@ -265,7 +266,7 @@ public class TvShowsController : BaseJellyfinApiController
return NotFound("Series not found"); return NotFound("Series not found");
} }
episodes = series.GetEpisodes(user, dtoOptions).ToList(); episodes = series.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes).ToList();
} }
// Filter after the fact in case the ui doesn't want them // Filter after the fact in case the ui doesn't want them

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
@ -173,6 +174,19 @@ public class VideosController : BaseJellyfinApiController
return NoContent(); return NoContent();
} }
private bool IsSubPath(string basePath, string testPath)
{
string normalizedBasePath = Path.GetFullPath(basePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string normalizedTestPath = Path.GetFullPath(testPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var isSubPath = normalizedTestPath.StartsWith(normalizedBasePath, StringComparison.OrdinalIgnoreCase)
&& normalizedTestPath.Length > normalizedBasePath.Length
&& (normalizedTestPath[normalizedBasePath.Length] == Path.DirectorySeparatorChar
|| normalizedTestPath[normalizedBasePath.Length] == Path.AltDirectorySeparatorChar);
return isSubPath;
}
/// <summary> /// <summary>
/// Merges videos into a single record. /// Merges videos into a single record.
/// </summary> /// </summary>
@ -198,11 +212,21 @@ public class VideosController : BaseJellyfinApiController
return BadRequest("Please supply at least two videos to merge."); return BadRequest("Please supply at least two videos to merge.");
} }
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId)); var primaryVersion = items.FirstOrDefault(i =>
{
if (i.GetMediaSourceCount((message) => _serverConfigurationManager.LogMessage(message)) > 1 && string.IsNullOrEmpty(i.PrimaryVersionId))
{
return true;
}
return false;
});
if (primaryVersion is null) if (primaryVersion is null)
{ {
primaryVersion = items primaryVersion = items
.OrderBy(i => .OrderBy(i => _serverConfigurationManager.FetchFolderPriority(i.GetContainingFolderPath()))
.ThenBy(i =>
{ {
if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile) if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
{ {

View File

@ -120,7 +120,12 @@ public static class RequestHelpers
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null) internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
{ {
userId ??= httpContext.User.GetUserId(); userId ??= httpContext.User.GetUserId();
var user = userManager.GetUserById(userId.Value); User? user = null;
if (!userId.IsNullOrEmpty())
{
user = userManager.GetUserById(userId.Value);
}
var session = await sessionManager.LogSessionActivity( var session = await sessionManager.LogSessionActivity(
httpContext.User.GetClient(), httpContext.User.GetClient(),
httpContext.User.GetVersion(), httpContext.User.GetVersion(),

View File

@ -142,6 +142,25 @@ public static class StreamingHelpers
} }
else else
{ {
// Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
// Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate,
// which will cause the client to request extremely high bitrate that may fail the player/encoder
streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate;
if (streamingRequest.SegmentContainer is not null)
{
// Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
// Notably: Some channels won't play on FireFox and LG webOS
// Some channels from HDHomerun will experience A/V sync issues
streamingRequest.SegmentContainer = "ts";
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);
mediaSource = liveStreamInfo.Item1; mediaSource = liveStreamInfo.Item1;
state.DirectStreamProvider = liveStreamInfo.Item2; state.DirectStreamProvider = liveStreamInfo.Item2;

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.0</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>

View File

@ -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,38 +121,25 @@ 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))
{
var devices = dbContext.Devices
.OrderBy(d => d.Id)
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value)) .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId) .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken); .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
.OrderBy(d => d.Id)
var count = await devices.CountAsync().ConfigureAwait(false); .ToList();
var count = devices.Count();
if (query.Skip.HasValue) if (query.Skip.HasValue)
{ {
@ -151,14 +151,13 @@ namespace Jellyfin.Server.Implementations.Devices
devices = devices.Take(query.Limit.Value); devices = devices.Take(query.Limit.Value);
} }
return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false)); 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,19 +166,13 @@ 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))
{
var sessions = dbContext.Devices
.Include(d => d.User)
.OrderByDescending(d => d.DateLastActivity) .OrderByDescending(d => d.DateLastActivity)
.ThenBy(d => d.DeviceId) .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.HasValue) if (!userId.IsNullOrEmpty())
{ {
var user = _userManager.GetUserById(userId.Value); var user = _userManager.GetUserById(userId.Value);
if (user is null) if (user is null)
@ -187,18 +180,22 @@ namespace Jellyfin.Server.Implementations.Devices
throw new ResourceNotFoundException(); throw new ResourceNotFoundException();
} }
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId)); devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
} }
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false); var array = devices.Select(device =>
{
_deviceOptions.TryGetValue(device.DeviceId, out var option);
return ToDeviceInfo(device, option);
}).ToArray();
return new QueryResult<DeviceInfo>(array); 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,

View File

@ -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;
@ -19,18 +18,10 @@ public static class ServiceCollectionExtensions
/// <returns>The updated service collection.</returns> /// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection) public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
{ {
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>();
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}") opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
}); });
return serviceCollection; return serviceCollection;

View File

@ -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" />

View File

@ -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

View File

@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
var options = _config.Configuration.TrickplayOptions; var options = _config.Configuration.TrickplayOptions;
if (options.Interval < 1000)
{
_logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval);
options.Interval = 1000;
}
foreach (var width in options.WidthResolutions) foreach (var width in options.WidthResolutions)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -121,6 +127,13 @@ public class TrickplayManager : ITrickplayManager
return; return;
} }
var mediaPath = mediaSource.Path;
if (!File.Exists(mediaPath))
{
_logger.LogWarning("Media source {MediaSourceId} not found at {Path} for item {ItemID}", mediaSource.Id, mediaPath, video.Id);
return;
}
// The width has to be even, otherwise a lot of filters will not be able to sample it // The width has to be even, otherwise a lot of filters will not be able to sample it
var actualWidth = 2 * (width / 2); var actualWidth = 2 * (width / 2);
@ -139,7 +152,6 @@ public class TrickplayManager : ITrickplayManager
return; return;
} }
var mediaPath = mediaSource.Path;
var mediaStream = mediaSource.VideoStream; var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container; var container = mediaSource.Container;
@ -261,7 +273,7 @@ public class TrickplayManager : ITrickplayManager
} }
// Update bitrate // Update bitrate
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000)); var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
} }

View File

@ -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)
{ {

View File

@ -1,7 +1,7 @@
#pragma warning disable CA1307 #pragma warning disable CA1307
#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -47,6 +47,8 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class. /// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary> /// </summary>
@ -84,30 +86,29 @@ namespace Jellyfin.Server.Implementations.Users
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateDbContext();
foreach (var user in dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsEnumerable())
{
_users.Add(user.Id, user);
}
} }
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated; public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<User> Users public IEnumerable<User> Users => _users.Values;
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext).ToList();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Guid> UsersIds public IEnumerable<Guid> UsersIds => _users.Keys;
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return dbContext.Users.Select(u => u.Id).ToList();
}
}
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
@ -123,8 +124,8 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
using var dbContext = _dbProvider.CreateDbContext(); _users.TryGetValue(id, out var user);
return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id)); return user;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -135,9 +136,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name)); throw new ArgumentException("Invalid username", nameof(name));
} }
using var dbContext = _dbProvider.CreateDbContext(); return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
return GetUsersInternal(dbContext)
.FirstOrDefault(u => string.Equals(u.Username, name));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -202,6 +201,8 @@ namespace Jellyfin.Server.Implementations.Users
user.AddDefaultPermissions(); user.AddDefaultPermissions();
user.AddDefaultPreferences(); user.AddDefaultPreferences();
_users.Add(user.Id, user);
return user; return user;
} }
@ -236,21 +237,12 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public async Task DeleteUserAsync(Guid userId) public async Task DeleteUserAsync(Guid userId)
{ {
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); if (!_users.TryGetValue(userId, out var user))
await using (dbContext.ConfigureAwait(false))
{
var user = await dbContext.Users
.AsSingleQuery()
.Include(u => u.Permissions)
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
.ConfigureAwait(false);
if (user is null)
{ {
throw new ResourceNotFoundException(nameof(userId)); throw new ResourceNotFoundException(nameof(userId));
} }
if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1) if (_users.Count == 1)
{ {
throw new InvalidOperationException(string.Format( throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -259,9 +251,7 @@ namespace Jellyfin.Server.Implementations.Users
} }
if (user.HasPermission(PermissionKind.IsAdministrator) if (user.HasPermission(PermissionKind.IsAdministrator)
&& await dbContext.Users && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
.CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
.ConfigureAwait(false) == 1)
{ {
throw new ArgumentException( throw new ArgumentException(
string.Format( string.Format(
@ -271,12 +261,17 @@ namespace Jellyfin.Server.Implementations.Users
nameof(userId)); nameof(userId));
} }
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Users.Remove(user); dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
_users.Remove(userId);
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
} }
}
/// <inheritdoc/> /// <inheritdoc/>
public Task ResetPassword(User user) public Task ResetPassword(User user)
@ -541,12 +536,9 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc /> /// <inheritdoc />
public async Task InitializeAsync() public async Task InitializeAsync()
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{ {
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist. // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
if (await dbContext.Users.AnyAsync().ConfigureAwait(false)) if (_users.Any())
{ {
return; return;
} }
@ -559,6 +551,9 @@ namespace Jellyfin.Server.Implementations.Users
_logger.LogWarning("No users, creating one with username {UserName}", defaultName); _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@ -605,9 +600,12 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var user = await GetUsersInternal(dbContext) var user = dbContext.Users
.FirstOrDefaultAsync(u => u.Id.Equals(userId)) .Include(u => u.Permissions)
.ConfigureAwait(false) .Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!"); ?? throw new ArgumentException("No user exists with given Id!");
user.SubtitleMode = config.SubtitleMode; user.SubtitleMode = config.SubtitleMode;
@ -635,6 +633,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user); dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
} }
@ -645,9 +644,12 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var user = await GetUsersInternal(dbContext) var user = dbContext.Users
.FirstOrDefaultAsync(u => u.Id.Equals(userId)) .Include(u => u.Permissions)
.ConfigureAwait(false) .Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!"); ?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
@ -708,6 +710,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user); dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
} }
@ -728,6 +731,7 @@ namespace Jellyfin.Server.Implementations.Users
} }
user.ProfileImage = null; user.ProfileImage = null;
_users[user.Id] = user;
} }
internal static void ThrowIfInvalidUsername(string name) internal static void ThrowIfInvalidUsername(string name)
@ -874,15 +878,8 @@ namespace Jellyfin.Server.Implementations.Users
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{ {
dbContext.Users.Update(user); dbContext.Users.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext)
=> dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage);
} }
} }

View File

@ -35,17 +35,18 @@ public static class WebHostBuilderExtensions
return builder return builder
.UseKestrel((builderContext, options) => .UseKestrel((builderContext, options) =>
{ {
var addresses = appHost.NetManager.GetAllBindInterfaces(true); var addresses = appHost.NetManager.GetAllBindInterfaces(false);
bool flagged = false; bool flagged = false;
foreach (var netAdd in addresses) foreach (var netAdd in addresses)
{ {
logger.LogInformation("Kestrel is listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All IPv6 addresses" : netAdd.Address); var address = netAdd.Address;
logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address);
options.Listen(netAdd.Address, appHost.HttpPort); options.Listen(netAdd.Address, appHost.HttpPort);
if (appHost.ListenWithHttps) if (appHost.ListenWithHttps)
{ {
options.Listen( options.Listen(
netAdd.Address, address,
appHost.HttpsPort, appHost.HttpsPort,
listenOptions => listenOptions.UseHttps(appHost.Certificate)); listenOptions => listenOptions.UseHttps(appHost.Certificate));
} }
@ -54,7 +55,7 @@ public static class WebHostBuilderExtensions
try try
{ {
options.Listen( options.Listen(
netAdd.Address, address,
appHost.HttpsPort, appHost.HttpsPort,
listenOptions => listenOptions.UseHttps()); listenOptions => listenOptions.UseHttps());
} }

View File

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

View File

@ -0,0 +1,106 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// Fixes the data column of audio types to be deserializable.
/// </summary>
internal class FixAudioData : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger<FixAudioData> _logger;
private readonly IServerApplicationPaths _applicationPaths;
private readonly IItemRepository _itemRepository;
public FixAudioData(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IItemRepository itemRepository)
{
_applicationPaths = applicationPaths;
_itemRepository = itemRepository;
_logger = loggerFactory.CreateLogger<FixAudioData>();
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}");
/// <inheritdoc/>
public string Name => "FixAudioData";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
// Back up the database before modifying any entries
for (int i = 1; ; i++)
{
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
if (!File.Exists(bakPath))
{
try
{
_logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
File.Copy(dbPath, bakPath);
_logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
throw;
}
}
}
_logger.LogInformation("Backfilling audio lyrics data to database.");
var startIndex = 0;
var records = _itemRepository.GetCount(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Audio],
});
while (startIndex < records)
{
var results = _itemRepository.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Audio],
StartIndex = startIndex,
Limit = 5000,
SkipDeserialization = true
})
.Cast<Audio>()
.ToList();
foreach (var audio in results)
{
var lyricMediaStreams = audio.GetMediaStreams().Where(s => s.Type == MediaStreamType.Lyric).Select(s => s.Path).ToList();
if (lyricMediaStreams.Count > 0)
{
audio.HasLyrics = true;
audio.LyricFiles = lyricMediaStreams;
}
}
_itemRepository.SaveItems(results, CancellationToken.None);
startIndex += results.Count;
_logger.LogInformation("Backfilled data for {UpdatedRecords} of {TotalRecords} audio records", startIndex, records);
}
}
}
}

View File

@ -67,7 +67,7 @@ namespace Jellyfin.Server.Migrations.Routines
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{ {
connection.Open(); connection.Open();
var dbContext = _provider.CreateDbContext(); using var dbContext = _provider.CreateDbContext();
var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); var queryResult = connection.Query("SELECT * FROM LocalUsersv2");

View File

@ -185,6 +185,7 @@ namespace Jellyfin.Server
} }
catch (Exception ex) catch (Exception ex)
{ {
_restartOnShutdown = false;
_logger.LogCritical(ex, "Error while starting server"); _logger.LogCritical(ex, "Error while starting server");
} }
finally finally

View File

@ -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.0</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>

View File

@ -58,6 +58,11 @@ public static class NetworkConstants
/// </summary> /// </summary>
public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16); public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
/// <summary>
/// IPv4 Link-Local as defined in RFC 3927.
/// </summary>
public static readonly IPNetwork IPv4RFC3927LinkLocal = new IPNetwork(IPAddress.Parse("169.254.0.0"), 16);
/// <summary> /// <summary>
/// IPv6 loopback as defined in RFC 4291. /// IPv6 loopback as defined in RFC 4291.
/// </summary> /// </summary>

View File

@ -19,5 +19,18 @@ namespace MediaBrowser.Controller.Configuration
/// </summary> /// </summary>
/// <value>The configuration.</value> /// <value>The configuration.</value>
ServerConfiguration Configuration { get; } ServerConfiguration Configuration { get; }
/// <summary>
/// Fetches the folder priority.
/// </summary>
/// <param name="path">The path to test.</param>
/// <returns>The priority int value (higher comes first).</returns>
int FetchFolderPriority(string path);
/// <summary>
/// Logs a message.
/// </summary>
/// <param name="message">The message to log.</param>
void LogMessage(string message);
} }
} }

View File

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

View File

@ -169,8 +169,7 @@ namespace MediaBrowser.Controller.Entities.Audio
var childUpdateType = ItemUpdateType.None; var childUpdateType = ItemUpdateType.None;
// Refresh songs only and not m3u files in album folder foreach (var item in items)
foreach (var item in items.OfType<Audio>())
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();

View File

@ -751,9 +751,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore] [JsonIgnore]
public virtual bool SupportsAncestors => true; public virtual bool SupportsAncestors => true;
[JsonIgnore]
public virtual bool StopRefreshIfLocalMetadataFound => true;
[JsonIgnore] [JsonIgnore]
protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol; protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
@ -1183,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);
} }
} }
@ -1952,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);
} }

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;
@ -364,15 +365,23 @@ namespace MediaBrowser.Controller.Entities
if (IsFileProtocol) if (IsFileProtocol)
{ {
IEnumerable<BaseItem> nonCachedChildren; IEnumerable<BaseItem> nonCachedChildren = [];
try try
{ {
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; return;
} }
@ -594,7 +603,7 @@ namespace MediaBrowser.Controller.Entities
} }
var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : 2 * Environment.ProcessorCount; var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount;
var actionBlock = new ActionBlock<int>( var actionBlock = new ActionBlock<int>(
async i => async i =>

View File

@ -51,6 +51,7 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>(); TrailerTypes = Array.Empty<TrailerType>();
VideoTypes = Array.Empty<VideoType>(); VideoTypes = Array.Empty<VideoType>();
Years = Array.Empty<int>(); Years = Array.Empty<int>();
SkipDeserialization = false;
} }
public InternalItemsQuery(User? user) public InternalItemsQuery(User? user)
@ -358,6 +359,8 @@ namespace MediaBrowser.Controller.Entities
public string? SeriesTimerId { get; set; } public string? SeriesTimerId { get; set; }
public bool SkipDeserialization { get; set; }
public void SetUser(User user) public void SetUser(User user)
{ {
MaxParentalRating = user.MaxParentalAgeRating; MaxParentalRating = user.MaxParentalAgeRating;

View File

@ -45,9 +45,6 @@ namespace MediaBrowser.Controller.Entities.Movies
set => TmdbCollectionName = value; set => TmdbCollectionName = value;
} }
[JsonIgnore]
public override bool StopRefreshIfLocalMetadataFound => false;
public override double GetDefaultPrimaryImageAspectRatio() public override double GetDefaultPrimaryImageAspectRatio()
{ {
// hack for tv plugins // hack for tv plugins

View File

@ -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()
{ {

View File

@ -159,7 +159,7 @@ namespace MediaBrowser.Controller.Entities.TV
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
var items = GetEpisodes(user, query.DtoOptions).Where(filter); var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
return PostFilterAndSort(items, query, false); return PostFilterAndSort(items, query, false);
} }
@ -169,30 +169,31 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary> /// </summary>
/// <param name="user">The user.</param> /// <param name="user">The user.</param>
/// <param name="options">The options to use.</param> /// <param name="options">The options to use.</param>
/// <param name="shouldIncludeMissingEpisodes">If missing episodes should be included.</param>
/// <returns>Set of episodes.</returns> /// <returns>Set of episodes.</returns>
public List<BaseItem> GetEpisodes(User user, DtoOptions options) public List<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{ {
return GetEpisodes(Series, user, options); return GetEpisodes(Series, user, options, shouldIncludeMissingEpisodes);
} }
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options) public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{ {
return GetEpisodes(series, user, null, options); return GetEpisodes(series, user, null, options, shouldIncludeMissingEpisodes);
} }
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options) public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{ {
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options); return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
} }
public List<BaseItem> GetEpisodes() public List<BaseItem> GetEpisodes()
{ {
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true)); return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
} }
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{ {
return GetEpisodes(user, new DtoOptions(true)); return GetEpisodes(user, new DtoOptions(true), true);
} }
protected override bool GetBlockUnratedValue(User user) protected override bool GetBlockUnratedValue(User user)

View File

@ -28,16 +28,12 @@ namespace MediaBrowser.Controller.Entities.TV
public Series() public Series()
{ {
AirDays = Array.Empty<DayOfWeek>(); AirDays = Array.Empty<DayOfWeek>();
SeasonNames = new Dictionary<int, string>();
} }
public DayOfWeek[] AirDays { get; set; } public DayOfWeek[] AirDays { get; set; }
public string AirTime { get; set; } public string AirTime { get; set; }
[JsonIgnore]
public Dictionary<int, string> SeasonNames { get; set; }
[JsonIgnore] [JsonIgnore]
public override bool SupportsAddingToPlaylist => true; public override bool SupportsAddingToPlaylist => true;
@ -73,9 +69,6 @@ namespace MediaBrowser.Controller.Entities.TV
/// <value>The status.</value> /// <value>The status.</value>
public SeriesStatus? Status { get; set; } public SeriesStatus? Status { get; set; }
[JsonIgnore]
public override bool StopRefreshIfLocalMetadataFound => false;
public override double GetDefaultPrimaryImageAspectRatio() public override double GetDefaultPrimaryImageAspectRatio()
{ {
double value = 2; double value = 2;
@ -257,7 +250,7 @@ namespace MediaBrowser.Controller.Entities.TV
return LibraryManager.GetItemsResult(query); return LibraryManager.GetItemsResult(query);
} }
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options) public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{ {
var seriesKey = GetUniqueSeriesKey(this); var seriesKey = GetUniqueSeriesKey(this);
@ -267,10 +260,10 @@ namespace MediaBrowser.Controller.Entities.TV
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season }, IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options DtoOptions = options,
}; };
if (user is null || !user.DisplayMissingEpisodes) if (!shouldIncludeMissingEpisodes)
{ {
query.IsMissing = false; query.IsMissing = false;
} }
@ -280,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV
var allSeriesEpisodes = allItems.OfType<Episode>().ToList(); var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
var allEpisodes = allItems.OfType<Season>() var allEpisodes = allItems.OfType<Season>()
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options)) .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
.Reverse(); .Reverse();
// Specials could appear twice based on above - once in season 0, once in the aired season // Specials could appear twice based on above - once in season 0, once in the aired season
@ -292,8 +285,7 @@ namespace MediaBrowser.Controller.Entities.TV
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{ {
// Refresh bottom up, children first, then the boxset // Refresh bottom up, seasons and episodes first, then the series
// By then hopefully the movies within will have Tmdb collection values
var items = GetRecursiveChildren(); var items = GetRecursiveChildren();
var totalItems = items.Count; var totalItems = items.Count;
@ -356,7 +348,7 @@ namespace MediaBrowser.Controller.Entities.TV
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
} }
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options) public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{ {
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons; var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
@ -373,24 +365,22 @@ namespace MediaBrowser.Controller.Entities.TV
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options DtoOptions = options
}; };
if (user is not null)
{ if (!shouldIncludeMissingEpisodes)
if (!user.DisplayMissingEpisodes)
{ {
query.IsMissing = false; query.IsMissing = false;
} }
}
var allItems = LibraryManager.GetItemList(query); var allItems = LibraryManager.GetItemList(query);
return GetSeasonEpisodes(parentSeason, user, allItems, options); return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
} }
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options) public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{ {
if (allSeriesEpisodes is null) if (allSeriesEpisodes is null)
{ {
return GetSeasonEpisodes(parentSeason, user, options); return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
} }
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons); var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);

View File

@ -23,9 +23,6 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>(); TrailerTypes = Array.Empty<TrailerType>();
} }
[JsonIgnore]
public override bool StopRefreshIfLocalMetadataFound => false;
public TrailerType[] TrailerTypes { get; set; } public TrailerType[] TrailerTypes { get; set; }
public override double GetDefaultPrimaryImageAspectRatio() public override double GetDefaultPrimaryImageAspectRatio()

View File

@ -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())
{ {

View File

@ -148,22 +148,7 @@ namespace MediaBrowser.Controller.Entities
public override bool SupportsAddingToPlaylist => true; public override bool SupportsAddingToPlaylist => true;
[JsonIgnore] [JsonIgnore]
public int MediaSourceCount public int MediaSourceCount => GetMediaSourceCount((message) => LibraryManager.LogMessage(message));
{
get
{
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
var item = LibraryManager.GetItemById(PrimaryVersionId);
if (item is Video video)
{
return video.MediaSourceCount;
}
}
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
}
}
[JsonIgnore] [JsonIgnore]
public bool IsStacked => AdditionalParts.Length > 0; public bool IsStacked => AdditionalParts.Length > 0;
@ -550,5 +535,44 @@ namespace MediaBrowser.Controller.Entities
return list; return list;
} }
/// <summary> Gets hte media source count.</summary>
/// <param name="logMessage">Logs a message.</param>
/// <returns>The mediasourcecount.</returns>
public int GetMediaSourceCount(Action<string> logMessage)
{
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
logMessage($"Got a PrimaryVersionId: {PrimaryVersionId}, {GetContainingFolderPath()}");
var item = LibraryManager.GetItemById(PrimaryVersionId);
if (item is Video video)
{
logMessage($"Entered the recursion!!!");
return video.MediaSourceCount;
}
}
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
}
/// <summary> Gets the containing folder path.</summary>
/// <returns>The path to the containing folder path.</returns>
public string GetContainingFolderPath()
{
if (IsStacked)
{
return System.IO.Path.GetDirectoryName(Path);
}
if (!IsPlaceHolder)
{
if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
{
return Path;
}
}
return base.ContainingFolderPath;
}
} }
} }

View File

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

View File

@ -149,6 +149,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken); Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Reloads the root media folder.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="removeRoot">Is remove the library itself allowed.</param>
/// <returns>Task.</returns>
Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false); Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
/// <summary> /// <summary>
@ -613,5 +621,11 @@ namespace MediaBrowser.Controller.Library
/// This exists so plugins can trigger a library scan. /// This exists so plugins can trigger a library scan.
/// </remarks> /// </remarks>
void QueueLibraryScan(); void QueueLibraryScan();
/// <summary>
/// Logs a message.
/// </summary>
/// <param name="message">The message to log.</param>
void LogMessage(string message);
} }
} }

View File

@ -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.0</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>

View File

@ -55,6 +55,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minKerneli915Hang = new Version(5, 18); private readonly Version _minKerneli915Hang = new Version(5, 18);
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3); private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18); private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
@ -680,16 +681,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return -1; return -1;
} }
public string GetInputPathArgument(EncodingJobInfo state)
{
return state.MediaSource.VideoType switch
{
VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
_ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
};
}
/// <summary> /// <summary>
/// Gets the audio encoder. /// Gets the audio encoder.
/// </summary> /// </summary>
@ -1005,7 +996,8 @@ namespace MediaBrowser.Controller.MediaEncoding
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc"); Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
if (IsVulkanFullSupported() if (IsVulkanFullSupported()
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{ {
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
@ -1197,13 +1189,14 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat"); var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
arg.Append(" -f concat -safe 0 -i ") arg.Append(" -f concat -safe 0 -i \"")
.Append(tmpConcatPath); .Append(tmpConcatPath)
.Append("\" ");
} }
else else
{ {
arg.Append(" -i ") arg.Append(" -i ")
.Append(GetInputPathArgument(state)); .Append(_mediaEncoder.GetInputPathArgument(state));
} }
// sub2video for external graphical subtitles // sub2video for external graphical subtitles
@ -1215,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))
@ -1320,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)))
{ {
@ -2012,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)
@ -2083,6 +2095,18 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_high"; profile = "constrained_high";
} }
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
&& profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_baseline";
}
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
&& profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_high";
}
if (!string.IsNullOrEmpty(profile)) if (!string.IsNullOrEmpty(profile))
{ {
// Currently there's no profile option in av1_nvenc encoder // Currently there's no profile option in av1_nvenc encoder
@ -2315,9 +2339,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// Video bitrate must fall within requested value // Video bitrate must fall within requested value
if (request.VideoBitRate.HasValue if (request.VideoBitRate.HasValue
&& (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)) && (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value))
{
// For LiveTV that has no bitrate, let's try copy if other conditions are met
if (string.IsNullOrWhiteSpace(request.LiveStreamId) || videoStream.BitRate.HasValue)
{ {
return false; return false;
} }
}
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec); var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
if (maxBitDepth.HasValue) if (maxBitDepth.HasValue)
@ -2629,10 +2657,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.AudioStream.Channels.HasValue && state.AudioStream.Channels.HasValue
&& state.AudioStream.Channels.Value == 6) && state.AudioStream.Channels.Value == 6)
{ {
if (!encodingOptions.DownMixAudioBoost.Equals(1))
{
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
}
switch (encodingOptions.DownMixStereoAlgorithm) switch (encodingOptions.DownMixStereoAlgorithm)
{ {
case DownMixStereoAlgorithms.Dave750: case DownMixStereoAlgorithms.Dave750:
filters.Add("volume=4.25");
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3"); filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
break; break;
case DownMixStereoAlgorithms.NightmodeDialogue: case DownMixStereoAlgorithms.NightmodeDialogue:
@ -2640,11 +2672,6 @@ namespace MediaBrowser.Controller.MediaEncoding
break; break;
case DownMixStereoAlgorithms.None: case DownMixStereoAlgorithms.None:
default: default:
if (!encodingOptions.DownMixAudioBoost.Equals(1))
{
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
}
break; break;
} }
} }
@ -2771,7 +2798,13 @@ namespace MediaBrowser.Controller.MediaEncoding
if (time > 0) if (time > 0)
{ {
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(time)); // For direct streaming/remuxing, we seek at the exact position of the keyframe
// However, ffmpeg will seek to previous keyframe when the exact time is the input
// Workaround this by adding 0.5s offset to the seeking time to get the exact keyframe on most videos.
// This will help subtitle syncing.
var isHlsRemuxing = state.IsVideoRequest && state.TranscodingType is TranscodingJobType.Hls && IsCopyCodec(state.OutputVideoCodec);
var seekTick = isHlsRemuxing ? time + 5000000L : time;
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick));
if (state.IsVideoRequest) if (state.IsVideoRequest)
{ {
@ -3155,7 +3188,9 @@ namespace MediaBrowser.Controller.MediaEncoding
int? requestedMaxHeight) int? requestedMaxHeight)
{ {
var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase); var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
var isMjpeg = videoEncoder is not null && videoEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var scaleVal = isV4l2 ? 64 : 2; var scaleVal = isV4l2 ? 64 : 2;
var targetAr = isMjpeg ? "(a*sar)" : "a"; // manually calculate AR when using mjpeg encoder
// If fixed dimensions were supplied // If fixed dimensions were supplied
if (requestedWidth.HasValue && requestedHeight.HasValue) if (requestedWidth.HasValue && requestedHeight.HasValue)
@ -3184,10 +3219,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2", @"scale=trunc(min(max(iw\,ih*{3})\,min({0}\,{1}*{3}))/{2})*{2}:trunc(min(max(iw/{3}\,ih)\,min({0}/{3}\,{1}))/2)*2",
maxWidthParam, maxWidthParam,
maxHeightParam, maxHeightParam,
scaleVal); scaleVal,
targetAr);
} }
// If a fixed width was requested // If a fixed width was requested
@ -3203,8 +3239,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"scale={0}:trunc(ow/a/2)*2", "scale={0}:trunc(ow/{1}/2)*2",
widthParam); widthParam,
targetAr);
} }
// If a fixed height was requested // If a fixed height was requested
@ -3214,9 +3251,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"scale=trunc(oh*a/{1})*{1}:{0}", "scale=trunc(oh*{2}/{1})*{1}:{0}",
heightParam, heightParam,
scaleVal); scaleVal,
targetAr);
} }
// If a max width was requested // If a max width was requested
@ -3226,9 +3264,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2", @"scale=trunc(min(max(iw\,ih*{2})\,{0})/{1})*{1}:trunc(ow/{2}/2)*2",
maxWidthParam, maxWidthParam,
scaleVal); scaleVal,
targetAr);
} }
// If a max height was requested // If a max height was requested
@ -3238,9 +3277,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})", @"scale=trunc(oh*{2}/{1})*{1}:min(max(iw/{2}\,ih)\,{0})",
maxHeightParam, maxHeightParam,
scaleVal); scaleVal,
targetAr);
} }
return string.Empty; return string.Empty;
@ -4285,6 +4325,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
// map from qsv to vaapi. // map from qsv to vaapi.
mainFilters.Add("hwmap=derive_device=vaapi"); mainFilters.Add("hwmap=derive_device=vaapi");
mainFilters.Add("format=vaapi");
} }
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
@ -4294,6 +4335,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
// map from vaapi to qsv. // map from vaapi to qsv.
mainFilters.Add("hwmap=derive_device=qsv"); mainFilters.Add("hwmap=derive_device=qsv");
mainFilters.Add("format=qsv");
} }
} }
@ -4468,7 +4510,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefered vaapi + vulkan filters pipeline // prefered vaapi + vulkan filters pipeline
if (_mediaEncoder.IsVaapiDeviceAmd if (_mediaEncoder.IsVaapiDeviceAmd
&& isVaapiVkSupported && isVaapiVkSupported
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop) && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{ {
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support. // AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);

View File

@ -245,6 +245,21 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>A playlist.</returns> /// <returns>A playlist.</returns>
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path); IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
/// <summary>
/// Gets the input path argument from <see cref="EncodingJobInfo"/>.
/// </summary>
/// <param name="state">The <see cref="EncodingJobInfo"/>.</param>
/// <returns>The input path argument.</returns>
string GetInputPathArgument(EncodingJobInfo state);
/// <summary>
/// Gets the input path argument.
/// </summary>
/// <param name="path">The item path.</param>
/// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
/// <returns>The input path argument.</returns>
string GetInputPathArgument(string path, MediaSourceInfo mediaSource);
/// <summary> /// <summary>
/// Generates a FFmpeg concat config for the source. /// Generates a FFmpeg concat config for the source.
/// </summary> /// </summary>

View File

@ -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
}; };

View File

@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem); return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
} }
public List<FileSystemMetadata> GetDirectories(string path)
{
var list = new List<FileSystemMetadata>();
var items = GetFileSystemEntries(path);
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
if (item.IsDirectory)
{
list.Add(item);
}
}
return list;
}
public List<FileSystemMetadata> GetFiles(string path) public List<FileSystemMetadata> GetFiles(string path)
{ {
var list = new List<FileSystemMetadata>(); var list = new List<FileSystemMetadata>();

View File

@ -9,6 +9,8 @@ namespace MediaBrowser.Controller.Providers
{ {
FileSystemMetadata[] GetFileSystemEntries(string path); FileSystemMetadata[] GetFileSystemEntries(string path);
List<FileSystemMetadata> GetDirectories(string path);
List<FileSystemMetadata> GetFiles(string path); List<FileSystemMetadata> GetFiles(string path);
FileSystemMetadata? GetFile(string path); FileSystemMetadata? GetFile(string path);

View File

@ -140,6 +140,14 @@ namespace MediaBrowser.Controller.Providers
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions) IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
where T : BaseItem; where T : BaseItem;
/// <summary>
/// Gets the metadata savers for the provided item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="libraryOptions">The library options.</param>
/// <returns>The metadata savers.</returns>
IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions);
/// <summary> /// <summary>
/// Gets all metadata plugins. /// Gets all metadata plugins.
/// </summary> /// </summary>

View File

@ -11,6 +11,8 @@ namespace MediaBrowser.Controller.Providers
public ItemInfo(BaseItem item) public ItemInfo(BaseItem item)
{ {
Path = item.Path; Path = item.Path;
ParentId = item.ParentId;
IndexNumber = item.IndexNumber;
ContainingFolderPath = item.ContainingFolderPath; ContainingFolderPath = item.ContainingFolderPath;
IsInMixedFolder = item.IsInMixedFolder; IsInMixedFolder = item.IsInMixedFolder;
@ -27,6 +29,10 @@ namespace MediaBrowser.Controller.Providers
public string Path { get; set; } public string Path { get; set; }
public Guid ParentId { get; set; }
public int? IndexNumber { get; set; }
public string ContainingFolderPath { get; set; } public string ContainingFolderPath { get; set; }
public VideoType VideoType { get; set; } public VideoType VideoType { get; set; }

View File

@ -38,19 +38,26 @@ 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()); var images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles); var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
if (metadataSubDir is not null)
{
var files = directoryService.GetFiles(metadataSubDir.FullName);
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
} }
private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles) return images;
}
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
{ {
var list = new List<LocalImageInfo>(1);
var thumbName = string.Concat(filenameWithoutExtension, "-thumb"); var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1); foreach (var i in filePaths)
foreach (var i in parentPathFiles)
{ {
if (i.IsDirectory) if (i.IsDirectory)
{ {

View File

@ -88,6 +88,19 @@ namespace MediaBrowser.MediaEncoding.Attachments
MediaSourceInfo mediaSource, MediaSourceInfo mediaSource,
string outputPath, string outputPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
if (shouldExtractOneByOne)
{
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
foreach (var i in attachmentIndexes)
{
var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
}
}
else
{ {
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{ {
@ -101,6 +114,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
} }
} }
} }
}
public async Task ExtractAllAttachmentsExternal( public async Task ExtractAllAttachmentsExternal(
string inputArgument, string inputArgument,
@ -270,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)
@ -309,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;

View File

@ -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)
{ {

View File

@ -30,10 +30,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Nikse.SubtitleEdit.Core.Common.IfoParser;
namespace MediaBrowser.MediaEncoding.Encoder namespace MediaBrowser.MediaEncoding.Encoder
{ {
@ -195,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()
@ -458,9 +456,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
extraArgs += " -probesize " + ffmpegProbeSize; extraArgs += " -probesize " + ffmpegProbeSize;
} }
if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent)) if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
{ {
extraArgs += " -user_agent " + userAgent; extraArgs += $" -user_agent \"{userAgent}\"";
} }
if (request.MediaSource.Protocol == MediaProtocol.Rtsp) if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
@ -621,13 +619,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
ImageFormat? targetFormat, ImageFormat? targetFormat,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var inputArgument = GetInputArgument(inputFile, mediaSource); var inputArgument = GetInputPathArgument(inputFile, mediaSource);
if (!isAudio) if (!isAudio)
{ {
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)
{ {
@ -639,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()
@ -665,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);
@ -724,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)
{ {
@ -824,6 +832,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
options.EnableTonemapping = false; options.EnableTonemapping = false;
} }
if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.AspectRatio))
{
// For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimensions
var darParts = imageStream.AspectRatio.Split(':');
var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], CultureInfo.InvariantCulture));
// When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
// Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, but we really can do little about it.
var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05;
if (shouldResetHeight)
{
// SAR = DAR * Height / Width
// RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
}
}
var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) }; var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive) var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
{ {
@ -871,6 +895,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new InvalidOperationException("Empty or invalid input argument."); throw new InvalidOperationException("Empty or invalid input argument.");
} }
float? encoderQuality = qualityScale;
if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
{
// vaapi's mjpeg encoder uses jpeg quality divided by QP2LAMBDA (118) as input, instead of ffmpeg defined qscale
// ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
// jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
encoderQuality = (100 - ((qualityScale - 1) * (100 / 30))) / 118;
}
// Output arguments // Output arguments
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(targetDirectory); Directory.CreateDirectory(targetDirectory);
@ -884,7 +917,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
filterParam, filterParam,
outputThreads.GetValueOrDefault(_threads), outputThreads.GetValueOrDefault(_threads),
vidEncoder, vidEncoder,
qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty, qualityScale.HasValue ? "-qscale:v " + encoderQuality.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
"image2", "image2",
outputPath); outputPath);
@ -936,7 +969,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs; var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs; timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
while (isResponsive) while (isResponsive && !cancellationToken.IsCancellationRequested)
{ {
try try
{ {
@ -950,8 +983,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
// We don't actually expect the process to be finished in one timeout span, just that one image has been generated. // We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
} }
cancellationToken.ThrowIfCancellationRequested();
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count(); var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
isResponsive = jpegCount > lastCount; isResponsive = jpegCount > lastCount;
@ -960,7 +991,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!ranToCompletion) if (!ranToCompletion)
{ {
_logger.LogInformation("Stopping trickplay extraction due to process inactivity."); if (!isResponsive)
{
_logger.LogInformation("Trickplay process unresponsive.");
}
_logger.LogInformation("Stopping trickplay extraction.");
StopProcess(processWrapper, 1000); StopProcess(processWrapper, 1000);
} }
} }
@ -1128,16 +1164,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files; var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
// Get all files from the BDMV/STREAMING directory // Get all files from the BDMV/STREAMING directory
var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
// Only return playable local .m2ts files // Only return playable local .m2ts files
return directoryFiles var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList();
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase)) return validPlaybackFiles
.Select(f => f.FullName) .Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName)
.Order() .Where(f => f is not null)
.ToList(); .ToList();
} }
/// <inheritdoc />
public string GetInputPathArgument(EncodingJobInfo state)
=> GetInputPathArgument(state.MediaPath, state.MediaSource);
/// <inheritdoc />
public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
{
return mediaSource.VideoType switch
{
VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null).ToList(), mediaSource),
VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path).ToList(), mediaSource),
_ => GetInputArgument(path, mediaSource)
};
}
/// <inheritdoc /> /// <inheritdoc />
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath) public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
{ {
@ -1158,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 var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
foreach (var path in files) foreach (var path in files)
{ {
var mediaInfoResult = GetMediaInfo( var mediaInfoResult = GetMediaInfo(
@ -1177,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);

View File

@ -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;
} }

View File

@ -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,

View File

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

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

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("dvdsub", StringComparison.OrdinalIgnoreCase)
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(codec, "sub", 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)

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.0</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>

View File

@ -43,7 +43,7 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the matched term. /// Gets or sets the matched term.
/// </summary> /// </summary>
/// <value>The matched term.</value> /// <value>The matched term.</value>
public string MatchedTerm { get; set; } public string? MatchedTerm { get; set; }
/// <summary> /// <summary>
/// Gets or sets the index number. /// Gets or sets the index number.

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;
@ -100,8 +101,8 @@ namespace MediaBrowser.Providers.Manager
{ {
saveLocally = false; saveLocally = false;
// If season is virtual under a physical series, save locally if using compatible convention // If season is virtual under a physical series, save locally
if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible) if (item is Season season)
{ {
var series = season.Series; var series = season.Series;
@ -126,7 +127,11 @@ namespace MediaBrowser.Providers.Manager
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally); var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false); string[] retryPaths = [];
if (saveLocally)
{
retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
}
// If there are more than one output paths, the stream will need to be seekable // If there are more than one output paths, the stream will need to be seekable
if (paths.Length > 1 && !source.CanSeek) if (paths.Length > 1 && !source.CanSeek)
@ -183,6 +188,29 @@ namespace MediaBrowser.Providers.Manager
try try
{ {
_fileSystem.DeleteFile(currentPath); _fileSystem.DeleteFile(currentPath);
// Remove local episode metadata directory if it exists and is empty
var directory = Path.GetDirectoryName(currentPath);
if (item is Episode && directory.Equals("metadata", StringComparison.Ordinal))
{
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)
{ {
@ -374,6 +402,47 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType)); throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
} }
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
extension = ".jpg";
}
extension = extension.ToLowerInvariant();
if (type == ImageType.Primary && saveLocally)
{
if (season is not null && season.IndexNumber.HasValue)
{
var seriesFolder = season.SeriesPath;
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-poster" + extension;
return Path.Combine(seriesFolder, imageFilename);
}
}
if (type == ImageType.Backdrop && saveLocally)
{
if (season is not null
&& season.IndexNumber.HasValue
&& (imageIndex is null || imageIndex == 0))
{
var seriesFolder = season.SeriesPath;
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
return Path.Combine(seriesFolder, imageFilename);
}
}
if (type == ImageType.Thumb && saveLocally) if (type == ImageType.Thumb && saveLocally)
{ {
if (season is not null && season.IndexNumber.HasValue) if (season is not null && season.IndexNumber.HasValue)
@ -447,20 +516,12 @@ namespace MediaBrowser.Providers.Manager
break; break;
} }
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
extension = ".jpg";
}
extension = extension.ToLowerInvariant();
string path = null; string path = null;
if (saveLocally) if (saveLocally)
{ {
if (type == ImageType.Primary && item is Episode) if (type == ImageType.Primary && item is Episode)
{ {
path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension); path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension);
} }
else if (item.IsInMixedFolder) else if (item.IsInMixedFolder)
{ {

View File

@ -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
@ -371,7 +370,7 @@ namespace MediaBrowser.Providers.Manager
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
// nothing to do, already gone // Nothing to do, already gone
} }
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
@ -381,6 +380,16 @@ namespace MediaBrowser.Providers.Manager
} }
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>
@ -413,12 +422,10 @@ 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) if (image is not null)
{ {
var currentImage = item.GetImageInfo(type, 0); var currentImage = item.GetImageInfo(type, 0);

View File

@ -8,7 +8,6 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -93,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))
@ -105,10 +100,19 @@ namespace MediaBrowser.Providers.Manager
} }
} }
// Start by validating images var localImagesFailed = false;
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
// Only validate already registered images if we are replacing and saving locally
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
{
item.ValidateImages();
}
else
{
// Run full image validation and register new local images
try try
{ {
// Always validate images and check for new locally stored ones.
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions)) if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
{ {
updateType |= ItemUpdateType.ImageUpdate; updateType |= ItemUpdateType.ImageUpdate;
@ -119,10 +123,12 @@ namespace MediaBrowser.Providers.Manager
localImagesFailed = true; localImagesFailed = true;
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name"); Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
} }
}
var metadataResult = new MetadataResult<TItemType> var metadataResult = new MetadataResult<TItemType>
{ {
Item = itemOfType Item = itemOfType,
People = LibraryManager.GetPeople(item)
}; };
bool hasRefreshedMetadata = true; bool hasRefreshedMetadata = true;
@ -154,7 +160,8 @@ namespace MediaBrowser.Providers.Manager
id.IsAutomated = refreshOptions.IsAutomated; id.IsAutomated = refreshOptions.IsAutomated;
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false); var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType; updateType |= result.UpdateType;
if (result.Failures > 0) if (result.Failures > 0)
@ -165,7 +172,7 @@ namespace MediaBrowser.Providers.Manager
} }
// Next run remote image providers, but only if local image providers didn't throw an exception // Next run remote image providers, but only if local image providers didn't throw an exception
if (!localImagesFailed && refreshOptions.ImageRefreshMode != MetadataRefreshMode.ValidationOnly) if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
{ {
var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList(); var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
@ -243,7 +250,7 @@ namespace MediaBrowser.Providers.Manager
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken) protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
{ {
if (result.Item.SupportsPeople && result.People is not null) if (result.Item.SupportsPeople)
{ {
var baseItem = result.Item; var baseItem = result.Item;
@ -399,7 +406,8 @@ namespace MediaBrowser.Providers.Manager
foreach (var child in children) foreach (var child in children)
{ {
if (!child.IsFolder) // Exclude any folders and virtual items since they are only placeholders
if (!child.IsFolder && !child.IsVirtualItem)
{ {
var childDateCreated = child.DateCreated; var childDateCreated = child.DateCreated;
if (childDateCreated > dateLastMediaAdded) if (childDateCreated > dateLastMediaAdded)
@ -638,6 +646,7 @@ namespace MediaBrowser.Providers.Manager
MetadataRefreshOptions options, MetadataRefreshOptions options,
ICollection<IMetadataProvider> providers, ICollection<IMetadataProvider> providers,
ItemImageProvider imageService, ItemImageProvider imageService,
bool isSavingMetadata,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var refreshResult = new RefreshResult var refreshResult = new RefreshResult
@ -655,26 +664,25 @@ namespace MediaBrowser.Providers.Manager
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false); await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
} }
if (item.IsLocked)
{
return refreshResult;
}
var temp = new MetadataResult<TItemType> var temp = new MetadataResult<TItemType>
{ {
Item = CreateNew() Item = CreateNew()
}; };
temp.Item.Path = item.Path; temp.Item.Path = item.Path;
temp.Item.Id = item.Id;
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
// If replacing all metadata, run internet providers first
if (options.ReplaceAllMetadata)
{
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false);
refreshResult.UpdateType |= remoteResult.UpdateType;
refreshResult.ErrorMessage = remoteResult.ErrorMessage;
refreshResult.Failures += remoteResult.Failures;
}
var hasLocalMetadata = false;
var foundImageTypes = new List<ImageType>(); var foundImageTypes = new List<ImageType>();
// Do not execute local providers if we are identifying or replacing with local metadata saving enabled
if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
{
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>()) foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{ {
var providerName = provider.GetType().Name; var providerName = provider.GetType().Name;
@ -720,15 +728,9 @@ namespace MediaBrowser.Providers.Manager
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
} }
MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true); MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport; refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
// Only one local provider allowed per item
if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item))
{
hasLocalMetadata = true;
}
break; break;
} }
@ -746,11 +748,12 @@ namespace MediaBrowser.Providers.Manager
refreshResult.ErrorMessage = ex.Message; refreshResult.ErrorMessage = ex.Message;
} }
} }
}
// Local metadata is king - if any is found don't run remote providers var isLocalLocked = temp.Item.IsLocked;
if (!options.ReplaceAllMetadata && (!hasLocalMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !item.StopRefreshIfLocalMetadataFound)) if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly))
{ {
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
refreshResult.UpdateType |= remoteResult.UpdateType; refreshResult.UpdateType |= remoteResult.UpdateType;
@ -762,19 +765,20 @@ namespace MediaBrowser.Providers.Manager
{ {
if (refreshResult.UpdateType > ItemUpdateType.None) if (refreshResult.UpdateType > ItemUpdateType.None)
{ {
if (hasLocalMetadata) if (!options.RemoveOldMetadata)
{
// Add existing metadata to provider result if it does not exist there
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
}
if (isLocalLocked)
{ {
MergeData(temp, metadata, item.LockedFields, true, true); MergeData(temp, metadata, item.LockedFields, true, true);
} }
else else
{ {
if (!options.RemoveOldMetadata) var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
{ MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
}
// Will always replace all metadata when Scan for new and updated files is used. Else, follow the options.
MergeData(temp, metadata, item.LockedFields, options.MetadataRefreshMode == MetadataRefreshMode.Default || options.ReplaceAllMetadata, false);
} }
} }
} }
@ -787,16 +791,6 @@ namespace MediaBrowser.Providers.Manager
return refreshResult; return refreshResult;
} }
protected virtual bool IsFullLocalMetadata(TItemType item)
{
if (string.IsNullOrWhiteSpace(item.Name))
{
return false;
}
return true;
}
private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken) private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
{ {
Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName); Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
@ -821,23 +815,20 @@ namespace MediaBrowser.Providers.Manager
return new TItemType(); return new TItemType();
} }
private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken) private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool replaceData, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
{ {
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);
@ -846,7 +837,7 @@ namespace MediaBrowser.Providers.Manager
{ {
result.Provider = provider.Name; result.Provider = provider.Name;
MergeData(result, temp, Array.Empty<MetadataField>(), false, false); MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false);
MergeNewData(temp.Item, id); MergeNewData(temp.Item, id);
refreshResult.UpdateType |= ItemUpdateType.MetadataDownload; refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
@ -948,13 +939,9 @@ namespace MediaBrowser.Providers.Manager
} }
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle)) if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
{
// Safeguard against incoming data having an empty name
if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
{ {
target.OriginalTitle = source.OriginalTitle; target.OriginalTitle = source.OriginalTitle;
} }
}
if (replaceData || !target.CommunityRating.HasValue) if (replaceData || !target.CommunityRating.HasValue)
{ {
@ -1016,7 +1003,7 @@ namespace MediaBrowser.Providers.Manager
{ {
targetResult.People = sourceResult.People; targetResult.People = sourceResult.People;
} }
else if (targetResult.People is not null && sourceResult.People is not null) else if (sourceResult.People is not null && sourceResult.People.Count >= 0)
{ {
MergePeople(sourceResult.People, targetResult.People); MergePeople(sourceResult.People, targetResult.People);
} }
@ -1049,6 +1036,10 @@ namespace MediaBrowser.Providers.Manager
{ {
target.Studios = source.Studios; target.Studios = source.Studios;
} }
else
{
target.Studios = target.Studios.Concat(source.Studios).Distinct().ToArray();
}
} }
if (!lockedFields.Contains(MetadataField.Tags)) if (!lockedFields.Contains(MetadataField.Tags))
@ -1057,6 +1048,10 @@ namespace MediaBrowser.Providers.Manager
{ {
target.Tags = source.Tags; target.Tags = source.Tags;
} }
else
{
target.Tags = target.Tags.Concat(source.Tags).Distinct().ToArray();
}
} }
if (!lockedFields.Contains(MetadataField.ProductionLocations)) if (!lockedFields.Contains(MetadataField.ProductionLocations))
@ -1065,6 +1060,10 @@ namespace MediaBrowser.Providers.Manager
{ {
target.ProductionLocations = source.ProductionLocations; target.ProductionLocations = source.ProductionLocations;
} }
else
{
target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
}
} }
foreach (var id in source.ProviderIds) foreach (var id in source.ProviderIds)
@ -1082,17 +1081,28 @@ namespace MediaBrowser.Providers.Manager
} }
} }
if (replaceData || !target.CriticRating.HasValue)
{
target.CriticRating = source.CriticRating;
}
if (replaceData || target.RemoteTrailers.Count == 0)
{
target.RemoteTrailers = source.RemoteTrailers;
}
else
{
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray();
}
MergeAlbumArtist(source, target, replaceData); MergeAlbumArtist(source, target, replaceData);
MergeCriticRating(source, target, replaceData);
MergeTrailers(source, target, replaceData);
MergeVideoInfo(source, target, replaceData); MergeVideoInfo(source, target, replaceData);
MergeDisplayOrder(source, target, replaceData); MergeDisplayOrder(source, target, replaceData);
if (replaceData || string.IsNullOrEmpty(target.ForcedSortName)) if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
{ {
var forcedSortName = source.ForcedSortName; var forcedSortName = source.ForcedSortName;
if (!string.IsNullOrEmpty(forcedSortName))
if (!string.IsNullOrWhiteSpace(forcedSortName))
{ {
target.ForcedSortName = forcedSortName; target.ForcedSortName = forcedSortName;
} }
@ -1100,22 +1110,44 @@ namespace MediaBrowser.Providers.Manager
if (mergeMetadataSettings) if (mergeMetadataSettings)
{ {
target.LockedFields = source.LockedFields; if (replaceData || !target.IsLocked)
target.IsLocked = source.IsLocked; {
target.IsLocked = target.IsLocked || source.IsLocked;
}
if (target.LockedFields.Length == 0)
{
target.LockedFields = source.LockedFields;
}
else
{
target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
}
// Grab the value if it's there, but if not then don't overwrite with the default
if (source.DateCreated != default) if (source.DateCreated != default)
{ {
target.DateCreated = source.DateCreated; target.DateCreated = source.DateCreated;
} }
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
{
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode; target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
}
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
{
target.PreferredMetadataLanguage = source.PreferredMetadataLanguage; target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
} }
} }
}
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target) private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
{ {
if (target is null)
{
target = new List<PersonInfo>();
}
foreach (var person in target) foreach (var person in target)
{ {
var normalizedName = person.Name.RemoveDiacritics(); var normalizedName = person.Name.RemoveDiacritics();
@ -1144,7 +1176,6 @@ namespace MediaBrowser.Providers.Manager
if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder)) if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
{ {
var displayOrder = sourceHasDisplayOrder.DisplayOrder; var displayOrder = sourceHasDisplayOrder.DisplayOrder;
if (!string.IsNullOrWhiteSpace(displayOrder)) if (!string.IsNullOrWhiteSpace(displayOrder))
{ {
targetHasDisplayOrder.DisplayOrder = displayOrder; targetHasDisplayOrder.DisplayOrder = displayOrder;
@ -1162,30 +1193,18 @@ namespace MediaBrowser.Providers.Manager
{ {
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists; targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
} }
} else if (sourceHasAlbumArtist.AlbumArtists.Count >= 0)
}
private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData)
{ {
if (replaceData || !target.CriticRating.HasValue) targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray();
{
target.CriticRating = source.CriticRating;
} }
} }
private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData)
{
if (replaceData || target.RemoteTrailers.Count == 0)
{
target.RemoteTrailers = source.RemoteTrailers;
}
} }
private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData) private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
{ {
if (source is Video sourceCast && target is Video targetCast) if (source is Video sourceCast && target is Video targetCast)
{ {
if (replaceData || targetCast.Video3DFormat is null) if (replaceData || !targetCast.Video3DFormat.HasValue)
{ {
targetCast.Video3DFormat = sourceCast.Video3DFormat; targetCast.Video3DFormat = sourceCast.Video3DFormat;
} }

View File

@ -418,6 +418,12 @@ namespace MediaBrowser.Providers.Manager
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false); return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
} }
/// <inheritdoc />
public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
{
return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false));
}
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata) private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
where T : BaseItem where T : BaseItem
{ {

Some files were not shown because too many files have changed in this diff Show More