Compare commits

...

136 Commits

Author SHA1 Message Date
Jellyfin Release Bot
10662e75e4 Bump version to 10.11.6 2026-01-18 20:02:59 -05:00
Joshua M. Boniface
a2b1936e73 Merge pull request #15816 from theguymadmax/preserve-artist-order
Fix artist display order
2026-01-18 19:48:17 -05:00
theguymadmax
2df546af6d Deduplicate using Distinct 2026-01-18 18:16:45 -05:00
Claus Vium
338b480217 Merge pull request #16046 from theguymadmax/restore-weekly-images
Restore weekly refresh for library folder images
2026-01-18 16:36:45 +01:00
theguymadmax
2943bb6fdd Restore collection folder image refresh 2026-01-18 01:51:51 -05:00
theguymadmax
94edcbd2d1 Fix artist ordering DtoServices 2026-01-17 10:14:41 -05:00
theguymadmax
a8d1cdefac Address review comments 2026-01-17 10:14:41 -05:00
Tim Eisele
a518160a6f Prioritize better matches on search (#15983) 2026-01-16 19:05:46 -07:00
Tim Eisele
b56de6493f Be more strict about PersonType assignments (#15872) 2026-01-16 19:03:13 -07:00
theguymadmax
093cfc3f3b Trim music artist names (#15808) 2026-01-16 18:51:48 -07:00
theguymadmax
49775b1f6a Fix birthplace not saving correctly (#16020) 2026-01-16 18:47:40 -07:00
Collin T Swisher
22d593b8e9 Add mblink creation logic to library update endpoint. (#15965) 2026-01-16 18:47:04 -07:00
theguymadmax
2cb7fb52d2 Skip hidden directories and .ignore paths in library monitoring (#16029) 2026-01-16 18:45:19 -07:00
Joshua M. Boniface
8433b6d8a4 Merge pull request #15899 from MarcoCoreDuo/fix-watch-state-not-kept
Fix watched state not kept on Media replace/rename
2026-01-16 16:40:36 -05:00
Bond-009
32d2414de0 Merge pull request #15950 from theguymadmax/revert-sort-index-number
Revert "always sort season by index number"
2026-01-09 18:38:23 +01:00
Bond-009
317a3a47c3 Merge pull request #15961 from theguymadmax/fix-bad-plugin-url
Fix crash when plugin repository has an invalid URL
2026-01-09 18:24:48 +01:00
theguymadmax
845b8cdc8f Fix crash when plugin repository has an invalid URL 2026-01-06 11:57:25 -05:00
theguymadmax
c86f6439c5 Revert "always sort season by index number"
This reverts commit e16ea7b236.
2026-01-05 11:06:25 -05:00
theguymadmax
559e0088e5 Fix tag inheritance for Continue Watching queries (#15931) 2026-01-04 11:20:34 -07:00
MarcoCoreDuo
adaca95590 make db context creation async 2025-12-31 07:43:07 +01:00
MarcoCoreDuo
09a1c31fa3 Refactor ReattachUserData methods to be asynchronous 2025-12-31 03:06:07 +01:00
MarcoCoreDuo
e4b82025b8 move reattaching user data to own function and call it only after fetching metadata for the first time 2025-12-30 22:04:59 +01:00
Collin T Swisher
78e3702cb0 Fix playlist item de-duplication (#15858) 2025-12-24 07:50:15 -07:00
Bond-009
01b20d3b75 Merge pull request #15833 from nyanmisaka/fix-h264-av1-sdr-hls-fallback
Fix missing H.264 and AV1 SDR fallbacks in HLS playlist
2025-12-24 10:28:33 +01:00
Tim Eisele
156761405e Prefer US rating on fallback (#15793) 2025-12-19 20:41:09 -07:00
Claus Vium
1805f2259f add CultureDto cache (#15826) 2025-12-19 20:38:54 -07:00
Nyanmisaka
4c587776d6 Fix the use of HWA in unsupported H.264 Hi422P/Hi444PP (#15819) 2025-12-19 19:58:56 -07:00
gnattu
8379b4634a Enforce more strict webm check (#15807) 2025-12-19 19:57:08 -07:00
Nyanmisaka
9470439cfa Fix video lacking SAR and DAR are marked as anamorphic (#15834) 2025-12-19 19:54:48 -07:00
gnattu
18096e48e0 Use hvc1 codectag for Dolby Vision 8.4 (#15835) 2025-12-19 19:53:28 -07:00
nyanmisaka
f2d0ac7b28 Fix missing H.264 and AV1 SDR fallbacks in HLS playlist
Previously, if HEVC encoding was disabled on the server,
SDR fallbacks would not be provided.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-12-19 20:33:24 +08:00
theguymadmax
2ccf08f547 Fix artist display order 2025-12-17 01:09:13 -05:00
Jellyfin Release Bot
1e27f460fe Bump version to 10.11.5 2025-12-14 21:44:14 -05:00
Andrew Rabert
4cdd8c8233 Fix unnecessary database JOINs in ApplyNavigations (#15666) 2025-12-13 10:58:08 -07:00
Tim Eisele
6e60634c9f Skip invalid ignore rules (#15746) 2025-12-13 08:39:49 -07:00
theguymadmax
12c5d6b636 Fix backdrop images being deleted when stored with media (#15766) 2025-12-13 08:29:17 -07:00
theguymadmax
b617c62f8e Fix NullReferenceException in ApplyOrder method (#15768) 2025-12-13 08:28:31 -07:00
Nyanmisaka
035b5895b0 Fix AV1 decoding hang regression on RK3588 (#15776) 2025-12-13 08:27:29 -07:00
theguymadmax
22da5187c8 Fix collection display order (#15767) 2025-12-13 08:27:01 -07:00
theguymadmax
5804d6840c Fix parental rating comparison with sub-scores (#15786) 2025-12-13 08:25:48 -07:00
Bond-009
b50ce1ad6b Merge pull request #15752 from Collin-Swish/fix-name-case-insensitivity
Fix case sensitivity edge case
2025-12-12 21:39:22 +01:00
Bond-009
481ee03f35 Merge pull request #15757 from theguymadmax/fix-trickplays-for-alt-versions
Fix trickplay images using wrong item on alternate versions
2025-12-12 21:31:52 +01:00
Bond-009
d91adb5d54 Merge pull request #15662 from SapientGuardian/issue15661
Fix blocking in async context in LimitedConcurrencyLibraryScheduler
2025-12-10 20:37:57 +01:00
theguymadmax
ef7f138a4e Fix trickplay images using wrong item on alternate versions 2025-12-09 14:21:09 -05:00
Collin Swisher
2e8d9a311b Fix case sensitivity edge case 2025-12-08 17:41:48 -06:00
gnattu
4c5a3fbff3 Use original name for MusicAritist matching (#15689) 2025-12-05 19:30:02 -07:00
liszto
636908fc4d Fix thumbnails not being deleted from temp folder 2025-12-05 19:29:54 -07:00
Tim Eisele
997362fc97 Backport dependency updates (#15723) 2025-12-05 19:27:30 -07:00
Noah Potash
c5147341e3 Fixes 15661. Replace BlockingCollection with Channel in LimitedConcurrencyLibraryScheduler to prevent blocking in an asynchronous context. 2025-12-03 21:50:08 -05:00
Noah Potash
ca33bcebf0 Add SapientGuardian to CONTRIBUTORS.md 2025-12-03 21:27:26 -05:00
Ivan Kara
d32f487e8e Fix symlinked file size (#15681) 2025-12-03 19:04:59 -07:00
theguymadmax
fb65f8f853 Fix ItemAdded event triggering when updating metadata (#15680) 2025-12-03 19:02:55 -07:00
martenumberto
2a0b90e385 Fix: Add .ts fallback for video streams to prevent crash (#15690) 2025-12-03 19:02:39 -07:00
myzhysz
dde70fd8a2 Fix stack overflow while scanning (#15698) 2025-12-03 19:02:04 -07:00
Niels van Velzen
98d1d0cb35 Merge pull request #15670 from nyanmisaka/fix-mjpeg-rk3576
Fix the empty output of trickplay on RK3576
2025-12-02 13:48:51 +01:00
Jellyfin Release Bot
ba76a8f3ad Bump version to 10.11.4 2025-11-30 21:33:32 -05:00
Anthony Lavado
8cd5652157 Merge pull request #15672 from jellyfin/openapi-cache-z
Cache OpenApi document generation
2025-11-30 21:22:29 -05:00
crobibero
8aff4227d9 Implement caching for OpenAPI document 2025-11-30 09:19:19 -07:00
nyanmisaka
026f7472cb Fix the empty output of trickplay on RK3576
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-11-30 21:38:47 +08:00
MBR-0001
daca285568 Revert "Localization/iso6392.txt: change pob and pop" (#15555) 2025-11-23 19:20:29 +01:00
theguymadmax
fbb9a0b2c7 Fix ResolveLinkTarget crashing on exFAT drives (#15568) 2025-11-21 21:14:39 -07:00
Ziyuan Qu
29b3aa8543 Add hidden file check in bdInfo (#15582) 2025-11-21 21:14:30 -07:00
theguymadmax
94f3725208 Fix isMovie filter logic (#15594) 2025-11-21 21:14:03 -07:00
theguymadmax
0ee81e87be Fix locked fields on not saving (#15564) 2025-11-19 17:02:53 +01:00
theguymadmax
c491a918c2 Save item to database before providers run to prevent FK constraint errors (#15563) 2025-11-19 17:01:13 +01:00
gnattu
1e7e46cb82 Prevent copying HDR streams when only SDR is supported (#15556) 2025-11-18 18:37:35 -07:00
theguymadmax
5ae444d96d Fix NullReferenceException in filesystem path comparison (#15548) 2025-11-18 18:37:09 -07:00
gnattu
ee7ad83427 Restrict first video frame probing to file protocol (#15557) 2025-11-18 18:36:59 -07:00
Jellyfin Release Bot
921d7d3364 Bump version to 10.11.3 2025-11-16 17:40:07 -05:00
theguymadmax
f8e012582a Fix movie titles using folder name when NFOs saver is enabled (#15529) 2025-11-16 13:59:58 -07:00
theguymadmax
def5956cd1 Fix tmdbid not detected in single movie folder (#14955) 2025-11-16 13:36:35 -07:00
theguymadmax
abfbaca336 Fix series DateLastMediaAdded not updating when new episodes are added (#15472) 2025-11-16 13:35:43 -07:00
theguymadmax
6566188e45 Add 1 minute tolerance for NFO change detection (#15514) 2025-11-15 08:39:25 -07:00
theguymadmax
078f9584ed Fix playlist DateCreated and DateLastMediaAdded not being set (#15508) 2025-11-14 15:19:40 -07:00
Iksas
ee34c75386 fix missing font extraction for certain transcoding settings (#15502) 2025-11-13 18:30:18 -07:00
theguymadmax
e8150428b6 Fix .ignore handling for directories (#15501) 2025-11-13 18:23:18 -07:00
theguymadmax
4b38e35bbb Remove InheritedTags and update tag filtering logic (#15493) 2025-11-13 18:23:03 -07:00
Huo Jiacheng
435bb14bb2 Fix gitignore-style not working properly on windows. (#15487) 2025-11-12 19:43:13 -07:00
theguymadmax
2e5ced5098 Improve season folder parsing (#15404) 2025-11-12 17:36:57 -07:00
Bond-009
f4a846aa4d Don't error out when searching for marker files fails (#15466)
Fixes #15445
2025-11-11 15:45:47 -07:00
Joshua M. Boniface
7c1063177f Merge pull request #15462 from theguymadmax/fix-exception-for-empty-strm-files
Fix NullReferenceException in GetPathProtocol when path is null
2025-11-10 19:30:38 -05:00
Joshua M. Boniface
5878b1ffc5 Merge pull request #15468 from Bond-009/carefulWithLastMinChanges
Check if target exists before trying to follow it
2025-11-10 19:12:24 -05:00
Bond_009
3c3c2aee0d Check if target exists before trying to follow it
Exception got caught in ManagedFileSystem and wrong file info got returned
2025-11-10 23:19:17 +01:00
theguymadmax
511223aac4 Fix NullReferenceException in GetPathProtocol when path is null 2025-11-10 02:30:49 -05:00
Mikal S.
3b2d64995a Resolve symlinks for static media source infos (#15263) 2025-11-09 09:45:02 -07:00
theguymadmax
13c4517a66 Fix collection grouping in mixed libraries (#15373) 2025-11-09 09:35:50 -07:00
theguymadmax
177b6464ca Don't clear baseitemids (#15446) 2025-11-09 09:22:09 -07:00
Bond-009
5a9a8363f4 Merge pull request #15441 from IceStormNG/fix-nullreference-role-null-10.11
Fix System.NullReferenceException when people's role is null (10.11.z)
2025-11-08 18:25:03 +01:00
theguymadmax
49efd68fc7 Invalidate parent folder's cache on deletion/creation (#15423) 2025-11-08 08:30:04 -07:00
Carsten Braun
90a8a26c6e Copy-Pasting is sometimes hard.... 2025-11-08 15:00:11 +01:00
Carsten Braun
002c83e6f5 Fix NullReferenceExceltop when role is null. 2025-11-08 14:32:14 +01:00
theguymadmax
7222910b05 Fix filters to use SortName (#15381) 2025-11-07 18:21:41 -07:00
Bond-009
097cb87f6f Don't enforce a minimum amount of free space for the tmp and log dirs (#15390) 2025-11-07 18:21:10 -07:00
JPVenson
91c3b1617e Fixed missing sort argument (#15413) 2025-11-07 18:20:42 -07:00
theguymadmax
8f71922734 Fix item count display for collapsed items (#15380) 2025-11-07 18:20:10 -07:00
Niels van Velzen
d140630208 Update branding in Swagger page (#15422) 2025-11-07 18:19:30 -07:00
theguymadmax
63a3e55297 Fix search terms using diacritics (#15435) 2025-11-07 18:18:24 -07:00
evanreichard
c2e5081d64 feat(sqlite): add timeout config (#15369) 2025-11-07 18:17:43 -07:00
Jellyfin Release Bot
4187c6f620 Bump version to 10.11.2 2025-11-02 21:28:56 -05:00
Tim Eisele
e7dbb3afec Skip too large extracted season numbers (#15326) 2025-11-02 09:11:48 -07:00
vinnyspb
f994dd6211 Update file size when refreshing metadata (#15325) 2025-11-01 14:18:19 -06:00
Cody Robibero
da254ee968 return instead of break, add check to more migrations (#15322) 2025-11-01 14:17:22 -06:00
Bill Thornton
4ad3141875 Update password reset to always return the same response structure (#15254) 2025-11-01 14:17:09 -06:00
evanreichard
b5f0199a25 fix: in optimistic locking, key off table is locked (#15328) 2025-11-01 14:15:26 -06:00
Nyanmisaka
6bf88c049e Ignore initial delay in audio-only containers (#15247) 2025-10-29 20:40:28 -06:00
Jellyfin Release Bot
40a33da2a5 Bump version to 10.11.1 2025-10-26 22:02:09 -04:00
Joshua M. Boniface
3596fc0693 Fix bump_version to handle spaced filename 2025-10-26 21:50:38 -04:00
Jellyfin Release Bot
93824dad97 Bump version to 10.11.1 2025-10-26 21:41:27 -04:00
Tim Eisele
e5656af1f2 Improve symlink handling (#15209) 2025-10-26 15:10:13 -06:00
Niels van Velzen
c127c10458 Merge pull request #15225 from Bond-009/z440ATL
Update dependency z440.atl.core to 7.6.0
2025-10-26 18:50:04 +01:00
Tim Eisele
7d1824ea27 Fix pagination and sorting for folders (#15187) 2025-10-26 11:34:11 -06:00
Cody Robibero
2966d27c97 Skip invalid database migration (#15212) 2025-10-26 11:34:04 -06:00
Ivan Kara
618ec4543e Add season number fallback for OMDB and TMDB plugins (#15113) 2025-10-26 11:33:55 -06:00
Cody Robibero
0e4031ae52 Skip extracting directory entry when restoring (#15196) 2025-10-26 11:33:47 -06:00
CeruleanRed
442af96ed9 Only save chapters that are within the runtime of the video file (#15176) 2025-10-26 10:37:16 -06:00
JJBlue
a305204cfa Skip extracted files in migration if bad timestamp or no access (#15220)
Fixes #15024
2025-10-26 10:30:43 -06:00
theguymadmax
75f472e6a7 Normalize paths in database queries (#15217) 2025-10-26 10:30:12 -06:00
Bond_009
cc32e8f7cb Update dependency z440.atl.core to 7.6.0 2025-10-26 15:16:08 +01:00
MBR-0001
14b3085ff1 Fix Has(Imdb/Tmdb/Tvdb)Id checks (#15126) 2025-10-25 16:00:55 -06:00
Cody Robibero
5691eee4f1 Prefer filting by package id instead of name (#15197) 2025-10-25 09:37:09 -06:00
theguymadmax
1520a697ad Play selected song first with instant mix (#15133) 2025-10-25 09:33:11 -06:00
Cody Robibero
81b8b0ca4a Add the transcode marker during startup instead of first transcode (#15194) 2025-10-25 09:32:15 -06:00
Cody Robibero
ac3fa3c376 Clean up backup service (#15170) 2025-10-24 17:57:34 -06:00
Tim Eisele
7a1c1cd342 Skip extracted files in migration if bad timestamp or no access (#15112) 2025-10-24 17:57:19 -06:00
gnattu
70c32a26fa Make priority class setting more robust (#15177) 2025-10-24 17:57:02 -06:00
Cody Robibero
2b94bb54aa Fix xml formatter (#15164) 2025-10-24 17:56:38 -06:00
Bond-009
0a6e8146be Lower required tmp dir size to 512MiB (#15098) 2025-10-23 16:38:27 -06:00
theguymadmax
305b0fdca3 Make season paths case-insensitive (#15102) 2025-10-23 16:38:06 -06:00
theguymadmax
d738386fe2 Fix LiveTV images not saving to database (#15083) 2025-10-23 16:37:55 -06:00
Tim Eisele
ca830d5be7 Speed-up trickplay migration (#15054) 2025-10-23 16:37:47 -06:00
theguymadmax
a5bc4524d8 Optimize artist query (#15087) 2025-10-23 16:37:29 -06:00
Nyanmisaka
175ee12bbc Fix videos with cropping metadata are probed as anamorphic (#15144) 2025-10-23 16:31:11 -06:00
Nyanmisaka
a725220c21 Reject stream copy of HDR10+ video if the client does not support HDR10 (#15072) 2025-10-21 17:20:56 -06:00
gnattu
a245605152 Log the message more clear when network manager is not ready (#15055) 2025-10-21 17:18:26 -06:00
Tim Eisele
f4a53209f4 Skip invalid keyframe cache data (#15032) 2025-10-21 17:17:56 -06:00
Jellyfin Release Bot
877251bcae Bump version to 10.11.0 2025-10-19 20:45:12 -04:00
111 changed files with 1982 additions and 1018 deletions

View File

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

View File

@@ -20,18 +20,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6

View File

@@ -11,13 +11,13 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
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 }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
@@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: abi-head name: abi-head
retention-days: 14 retention-days: 14
@@ -40,14 +40,14 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
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 }}
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
@@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: abi-base name: abi-base
retention-days: 14 retention-days: 14
@@ -85,13 +85,13 @@ jobs:
steps: steps:
- name: Download abi-head - name: Download abi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: abi-head name: abi-head
path: abi-head path: abi-head
- name: Download abi-base - name: Download abi-base
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: abi-base name: abi-base
path: abi-base path: abi-base

View File

@@ -16,18 +16,18 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
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 }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
- name: Generate openapi.json - name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json - name: Upload openapi.json
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: openapi-head name: openapi-head
retention-days: 14 retention-days: 14
@@ -41,7 +41,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
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 }}
@@ -55,13 +55,13 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
- name: Generate openapi.json - name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json - name: Upload openapi.json
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: openapi-base name: openapi-base
retention-days: 14 retention-days: 14
@@ -80,12 +80,12 @@ jobs:
- openapi-base - openapi-base
steps: steps:
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
- name: Download openapi-base - name: Download openapi-base
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: openapi-base name: openapi-base
path: openapi-base path: openapi-base
@@ -158,7 +158,7 @@ jobs:
run: |- run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
@@ -172,7 +172,7 @@ jobs:
strip_components: 1 strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place - name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"
@@ -220,7 +220,7 @@ jobs:
run: |- run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
@@ -234,7 +234,7 @@ jobs:
strip_components: 1 strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place - name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"

View File

@@ -20,9 +20,9 @@ jobs:
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: ${{ env.SDK_VERSION }} dotnet-version: ${{ env.SDK_VERSION }}
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal --verbosity minimal
- name: Merge code coverage results - name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17 uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@@ -40,11 +40,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.14' python-version: '3.14'
cache: 'pip' cache: 'pip'

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true ascending: true

View File

@@ -10,11 +10,11 @@ jobs:
issues: write issues: write
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.14' python-version: '3.14'
cache: 'pip' cache: 'pip'

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true ascending: true

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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}

View File

@@ -117,6 +117,7 @@
- [sachk](https://github.com/sachk) - [sachk](https://github.com/sachk)
- [sammyrc34](https://github.com/sammyrc34) - [sammyrc34](https://github.com/sammyrc34)
- [samuel9554](https://github.com/samuel9554) - [samuel9554](https://github.com/samuel9554)
- [SapientGuardian](https://github.com/SapientGuardian)
- [scheidleon](https://github.com/scheidleon) - [scheidleon](https://github.com/scheidleon)
- [sebPomme](https://github.com/sebPomme) - [sebPomme](https://github.com/sebPomme)
- [SegiH](https://github.com/SegiH) - [SegiH](https://github.com/SegiH)
@@ -205,6 +206,8 @@
- [theshoeshiner](https://github.com/theshoeshiner) - [theshoeshiner](https://github.com/theshoeshiner)
- [TokerX](https://github.com/TokerX) - [TokerX](https://github.com/TokerX)
- [GeneMarks](https://github.com/GeneMarks) - [GeneMarks](https://github.com/GeneMarks)
- [martenumberto](https://github.com/martenumberto)
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
# Emby Contributors # Emby Contributors

View File

@@ -4,7 +4,7 @@
</PropertyGroup> </PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies"> <ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" /> <PackageVersion Include="AsyncKeyedLock" Version="7.1.8" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -17,7 +17,7 @@
<PackageVersion Include="Diacritics" Version="4.0.17" /> <PackageVersion Include="Diacritics" Version="4.0.17" />
<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="FsCheck.Xunit" Version="3.3.1" /> <PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
<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.8" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
@@ -26,33 +26,33 @@
<PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.10" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.10" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
@@ -62,13 +62,13 @@
<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.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" /> <PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Polly" Version="8.6.4" /> <PackageVersion Include="Polly" Version="8.6.5" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> <PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="7.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" />
@@ -84,11 +84,11 @@
<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.3" /> <PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" /> <PackageVersion Include="System.Text.Json" Version="9.0.11" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.5.0" /> <PackageVersion Include="z440.atl.core" Version="7.9.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
@@ -96,4 +96,4 @@
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" /> <PackageVersion Include="xunit" Version="2.9.3" />
</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.11.0</VersionPrefix> <VersionPrefix>10.11.6</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

@@ -10,12 +10,17 @@ namespace Emby.Naming.TV
/// </summary> /// </summary>
public static partial class SeasonPathParser public static partial class SeasonPathParser
{ {
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$")] private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPre(); private static partial Regex ProcessPre();
[GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")] [GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPost(); private static partial Regex ProcessPost();
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
private static partial Regex SeasonPrefix();
/// <summary> /// <summary>
/// Attempts to parse season number from path. /// Attempts to parse season number from path.
/// </summary> /// </summary>
@@ -56,44 +61,34 @@ namespace Emby.Naming.TV
bool supportSpecialAliases, bool supportSpecialAliases,
bool supportNumericSeasonFolders) bool supportNumericSeasonFolders)
{ {
string filename = Path.GetFileName(path); var fileName = Path.GetFileName(path);
filename = Regex.Replace(filename, "[ ._-]", string.Empty);
var seasonPrefixMatch = SeasonPrefix().Match(fileName);
if (seasonPrefixMatch.Success &&
int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return (val, true);
}
string filename = CleanNameRegex.Replace(fileName, string.Empty);
if (parentFolderName is not null) if (parentFolderName is not null)
{ {
parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty); var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty);
filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase); filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase);
} }
if (supportSpecialAliases) if (supportSpecialAliases &&
(filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
{ {
if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) return (0, true);
{
return (0, true);
}
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
{
return (0, true);
}
} }
if (supportNumericSeasonFolders) if (supportNumericSeasonFolders &&
int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
{ {
if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) return (val, true);
{
return (val, true);
}
}
if (filename.StartsWith('s'))
{
var testFilename = filename.AsSpan()[1..];
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return (val, true);
}
} }
var preMatch = ProcessPre().Match(filename); var preMatch = ProcessPre().Match(filename);
@@ -113,8 +108,10 @@ namespace Emby.Naming.TV
var numberString = match.Groups["seasonnumber"]; var numberString = match.Groups["seasonnumber"];
if (numberString.Success) if (numberString.Success)
{ {
var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture); if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
return (seasonNumber, true); {
return (seasonNumber, true);
}
} }
return (null, false); return (null, false);

View File

@@ -107,10 +107,20 @@ namespace Emby.Server.Implementations.AppBase
private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
{ {
var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); string? otherMarkers = null;
try
{
otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => !Path.GetFileName(e.AsSpan()).Equals(markerName, StringComparison.OrdinalIgnoreCase));
}
catch
{
// Error while checking for marker files, assume none exist and keep going
// TODO: add some logging
}
if (otherMarkers is not null) if (otherMarkers is not null)
{ {
throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); throw new InvalidOperationException($"Expected to find only {markerName} but found marker for {otherMarkers}.");
} }
var markerPath = Path.Combine(path, markerName); var markerPath = Path.Combine(path, markerName);

View File

@@ -223,7 +223,7 @@ public class ChapterManager : IChapterManager
if (saveChapters && changesMade) if (saveChapters && changesMade)
{ {
_chapterRepository.SaveChapters(video.Id, chapters); SaveChapters(video, chapters);
} }
DeleteDeadImages(currentImages, chapters); DeleteDeadImages(currentImages, chapters);
@@ -234,7 +234,9 @@ public class ChapterManager : IChapterManager
/// <inheritdoc /> /// <inheritdoc />
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters) public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
{ {
_chapterRepository.SaveChapters(video.Id, chapters); // Remove any chapters that are outside of the runtime of the video
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
_chapterRepository.SaveChapters(video.Id, validChapters);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -1051,16 +1051,16 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor // Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
.Where(e => e.Value.Length > 0)
.Select(i => dto.ArtistItems = hasArtist.Artists
{ .Where(name => !string.IsNullOrWhiteSpace(name))
return new NameGuidPair .Distinct()
{ .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
Name = i.Key, ? new NameGuidPair { Name = name, Id = artists[0].Id }
Id = i.Value.First().Id : null)
}; .Where(item => item is not null)
}).Where(i => i is not null).ToArray(); .ToArray();
} }
if (item is IHasAlbumArtist hasAlbumArtist) if (item is IHasAlbumArtist hasAlbumArtist)
@@ -1085,31 +1085,16 @@ namespace Emby.Server.Implementations.Dto
// }) // })
// .ToList(); // .ToList();
var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.AlbumArtists = hasAlbumArtist.AlbumArtists dto.AlbumArtists = hasAlbumArtist.AlbumArtists
// .Except(foundArtists, new DistinctNameComparer()) .Where(name => !string.IsNullOrWhiteSpace(name))
.Select(i => .Distinct()
{ .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0
// This should not be necessary but we're seeing some cases of it ? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
if (string.IsNullOrEmpty(i)) : null)
{ .Where(item => item is not null)
return null; .ToArray();
}
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
{
EnableImages = false
});
if (artist is not null)
{
return new NameGuidPair
{
Name = artist.Name,
Id = artist.Id
};
}
return null;
}).Where(i => i is not null).ToArray();
} }
// Add video info // Add video info

View File

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

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Security; using System.Security;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -152,6 +153,10 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc /> /// <inheritdoc />
public void MoveDirectory(string source, string destination) public void MoveDirectory(string source, string destination)
{ {
// Make sure parent directory of target exists
var parent = Directory.GetParent(destination);
parent?.Create();
try try
{ {
Directory.Move(source, destination); Directory.Move(source, destination);
@@ -248,47 +253,40 @@ namespace Emby.Server.Implementations.IO
{ {
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory; result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
// {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
// }
if (info is FileInfo fileInfo) if (info is FileInfo fileInfo)
{ {
result.Length = fileInfo.Length; result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes! if (fileInfo.LinkTarget is not null)
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{ {
try try
{ {
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true);
if (targetFileInfo is not null)
{ {
result.Length = RandomAccess.GetLength(fileHandle); result.Exists = targetFileInfo.Exists;
if (result.Exists)
{
result.Length = targetFileInfo.Length;
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
}
}
else
{
result.Exists = false;
} }
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
} }
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName); _logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
} }
catch (IOException ex) }
{ else
// IOException generally means the file is not accessible due to filesystem issues {
// Catch this exception and mark the file as not exist to ignore it result.Length = fileInfo.Length;
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
result.Exists = false;
}
} }
} }
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
} }
else else
{ {
@@ -499,8 +497,17 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc /> /// <inheritdoc />
public virtual bool AreEqual(string path1, string path2) public virtual bool AreEqual(string path1, string path2)
{ {
return Path.TrimEndingDirectorySeparator(path1).Equals( if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
Path.TrimEndingDirectorySeparator(path2), {
return false;
}
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
return string.Equals(
normalized1,
normalized2,
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
} }

View File

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

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@@ -11,28 +13,24 @@ namespace Emby.Server.Implementations.Library;
/// </summary> /// </summary>
public class DotIgnoreIgnoreRule : IResolverIgnoreRule public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{ {
private static readonly bool IsWindows = OperatingSystem.IsWindows();
private static FileInfo? FindIgnoreFile(DirectoryInfo directory) private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
{ {
var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); for (var current = directory; current is not null; current = current.Parent)
if (ignoreFile.Exists)
{ {
return ignoreFile; var ignorePath = Path.Join(current.FullName, ".ignore");
if (File.Exists(ignorePath))
{
return new FileInfo(ignorePath);
}
} }
var parentDir = directory.Parent; return null;
if (parentDir is null)
{
return null;
}
return FindIgnoreFile(parentDir);
} }
/// <inheritdoc /> /// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
{
return IsIgnored(fileInfo, parent);
}
/// <summary> /// <summary>
/// Checks whether or not the file is ignored. /// Checks whether or not the file is ignored.
@@ -42,60 +40,101 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <returns>True if the file should be ignored.</returns> /// <returns>True if the file should be ignored.</returns>
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{ {
if (fileInfo.IsDirectory) var searchDirectory = fileInfo.IsDirectory
{ ? new DirectoryInfo(fileInfo.FullName)
var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName)); : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
if (dirIgnoreFile is null)
{
return false;
}
// Fast path in case the ignore files isn't a symlink and is empty if (string.IsNullOrEmpty(searchDirectory.FullName))
if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0
&& dirIgnoreFile.Length == 0)
{
return true;
}
// ignore the directory only if the .ignore file is empty
// evaluate individual files otherwise
return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
}
var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
if (string.IsNullOrEmpty(parentDirPath))
{ {
return false; return false;
} }
var folder = new DirectoryInfo(parentDirPath); var ignoreFile = FindIgnoreFile(searchDirectory);
var ignoreFile = FindIgnoreFile(folder);
if (ignoreFile is null) if (ignoreFile is null)
{ {
return false; return false;
} }
string ignoreFileString = GetFileContent(ignoreFile); // Fast path in case the ignore files isn't a symlink and is empty
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
if (string.IsNullOrWhiteSpace(ignoreFileString))
{ {
// Ignore directory if we just have the file // Ignore directory if we just have the file
return true; return true;
} }
// If file has content, base ignoring off the content .gitignore-style rules var content = GetFileContent(ignoreFile);
var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); return string.IsNullOrWhiteSpace(content)
var ignore = new Ignore.Ignore(); || CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
ignore.Add(ignoreRules);
return ignore.IsIgnored(fileInfo.FullName);
} }
private static string GetFileContent(FileInfo dirIgnoreFile) private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
{ {
using (var reader = dirIgnoreFile.OpenText()) // If file has content, base ignoring off the content .gitignore-style rules
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return CheckIgnoreRules(path, rules, isDirectory);
}
/// <summary>
/// Checks whether a path should be ignored based on an array of ignore rules.
/// </summary>
/// <param name="path">The path to check.</param>
/// <param name="rules">The array of ignore rules.</param>
/// <param name="isDirectory">Whether the path is a directory.</param>
/// <returns>True if the path should be ignored.</returns>
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
=> CheckIgnoreRules(path, rules, isDirectory, IsWindows);
/// <summary>
/// Checks whether a path should be ignored based on an array of ignore rules.
/// </summary>
/// <param name="path">The path to check.</param>
/// <param name="rules">The array of ignore rules.</param>
/// <param name="isDirectory">Whether the path is a directory.</param>
/// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
/// <returns>True if the path should be ignored.</returns>
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
{
var ignore = new Ignore.Ignore();
// Add each rule individually to catch and skip invalid patterns
var validRulesAdded = 0;
foreach (var rule in rules)
{ {
return reader.ReadToEnd(); try
{
ignore.Add(rule);
validRulesAdded++;
}
catch (RegexParseException)
{
// Ignore invalid patterns
}
} }
// If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
if (validRulesAdded == 0)
{
return true;
}
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
// See https://github.com/jellyfin/jellyfin/issues/15484
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
// Add trailing slash for directories to match "folder/"
if (isDirectory)
{
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
}
return ignore.IsIgnored(pathToCheck);
}
private static string GetFileContent(FileInfo ignoreFile)
{
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
return ignoreFile.Exists
? File.ReadAllText(ignoreFile.FullName)
: string.Empty;
} }
} }

View File

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

View File

@@ -457,6 +457,12 @@ namespace Emby.Server.Implementations.Library
_cache.TryRemove(child.Id, out _); _cache.TryRemove(child.Id, out _);
} }
if (parent is Folder folder)
{
folder.Children = null;
folder.UserData = null;
}
ReportItemRemoved(item, parent); ReportItemRemoved(item, parent);
} }
@@ -1052,6 +1058,7 @@ namespace Emby.Server.Implementations.Library
{ {
IncludeItemTypes = [BaseItemKind.MusicArtist], IncludeItemTypes = [BaseItemKind.MusicArtist],
Name = name, Name = name,
UseRawName = true,
DtoOptions = options DtoOptions = options
}).Cast<MusicArtist>() }).Cast<MusicArtist>()
.OrderBy(i => i.IsAccessedByName ? 1 : 0) .OrderBy(i => i.IsAccessedByName ? 1 : 0)
@@ -1993,6 +2000,12 @@ namespace Emby.Server.Implementations.Library
RegisterItem(item); RegisterItem(item);
} }
if (parent is Folder folder)
{
folder.Children = null;
folder.UserData = null;
}
if (ItemAdded is not null) if (ItemAdded is not null)
{ {
foreach (var item in items) foreach (var item in items)
@@ -2150,6 +2163,12 @@ namespace Emby.Server.Implementations.Library
_itemRepository.SaveItems(items, cancellationToken); _itemRepository.SaveItems(items, cancellationToken);
if (parent is Folder folder)
{
folder.Children = null;
folder.UserData = null;
}
if (ItemUpdated is not null) if (ItemUpdated is not null)
{ {
foreach (var item in items) foreach (var item in items)
@@ -2183,6 +2202,12 @@ namespace Emby.Server.Implementations.Library
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken); => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
{
await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
}
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
@@ -3176,19 +3201,7 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var shortcutFilename = Path.GetFileNameWithoutExtension(path); CreateShortcut(virtualFolderPath, pathInfo);
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
while (File.Exists(lnk))
{
shortcutFilename += "1";
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
}
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
if (saveLibraryOptions) if (saveLibraryOptions)
{ {
@@ -3353,5 +3366,24 @@ namespace Emby.Server.Implementations.Library
return item is UserRootFolder || item.IsVisibleStandalone(user); return item is UserRootFolder || item.IsVisibleStandalone(user);
} }
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
{
var path = pathInfo.Path;
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
while (File.Exists(lnk))
{
shortcutFilename += "1";
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
}
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
}
} }
} }

View File

@@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />> /// <inheritdoc />>
public MediaProtocol GetPathProtocol(string path) public MediaProtocol GetPathProtocol(string path)
{ {
if (string.IsNullOrEmpty(path))
{
return MediaProtocol.File;
}
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
{ {
return MediaProtocol.Rtsp; return MediaProtocol.Rtsp;

View File

@@ -28,7 +28,9 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{ {
return GetInstantMixFromGenres(item.Genres, user, dtoOptions); var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -369,13 +369,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// We need to only look at the name of this actual item (not parents) // We need to only look at the name of this actual item (not parents)
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan()); var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
if (!justName.IsEmpty) var tmdbid = justName.GetAttributeValue("tmdbid");
// If not in a mixed folder and ID not found in folder path, check filename
if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
{ {
// Check for TMDb id tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
var tmdbid = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
} }
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
if (!string.IsNullOrEmpty(item.Path)) if (!string.IsNullOrEmpty(item.Path))
{ {
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name) // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)

View File

@@ -38,6 +38,7 @@ namespace Emby.Server.Implementations.Localization
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly ConcurrentDictionary<string, CultureDto?> _cultureCache = new(StringComparer.OrdinalIgnoreCase);
private List<CultureDto> _cultures = []; private List<CultureDto> _cultures = [];
private FrozenDictionary<string, string> _iso6392BtoT = null!; private FrozenDictionary<string, string> _iso6392BtoT = null!;
@@ -161,6 +162,7 @@ namespace Emby.Server.Implementations.Localization
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames)); list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
} }
_cultureCache.Clear();
_cultures = list; _cultures = list;
_iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
} }
@@ -169,20 +171,31 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc /> /// <inheritdoc />
public CultureDto? FindLanguageInfo(string language) public CultureDto? FindLanguageInfo(string language)
{ {
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs if (string.IsNullOrEmpty(language))
for (var i = 0; i < _cultures.Count; i++)
{ {
var culture = _cultures[i]; return null;
if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|| language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|| culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
|| language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
{
return culture;
}
} }
return default; return _cultureCache.GetOrAdd(
language,
static (lang, cultures) =>
{
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
for (var i = 0; i < cultures.Count; i++)
{
var culture = cultures[i];
if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|| lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|| culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase)
|| lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
{
return culture;
}
}
return null;
},
_cultures);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -311,15 +324,19 @@ namespace Emby.Server.Implementations.Localization
else else
{ {
// Fall back to server default language for ratings check // Fall back to server default language for ratings check
// If it has no ratings, use the US ratings var ratingsDictionary = GetParentalRatingsDictionary();
var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
{ {
return value; return value;
} }
} }
// If we don't find anything, check all ratings systems // If we don't find anything, check all ratings systems, starting with US
if (_allParentalRatings.TryGetValue("us", out var usRatings) && usRatings.TryGetValue(rating, out var usValue))
{
return usValue;
}
foreach (var dictionary in _allParentalRatings.Values) foreach (var dictionary in _allParentalRatings.Values)
{ {
if (dictionary.TryGetValue(rating, out var value)) if (dictionary.TryGetValue(rating, out var value))

View File

@@ -347,8 +347,8 @@ pli||pi|Pali|pali
pol||pl|Polish|polonais pol||pl|Polish|polonais
pon|||Pohnpeian|pohnpei pon|||Pohnpeian|pohnpei
por||pt|Portuguese|portugais por||pt|Portuguese|portugais
por||pt-pt|Portuguese (Portugal)|portugais (pt-pt) pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
por||pt-br|Portuguese (Brazil)|portugais (pt-br) pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
pra|||Prakrit languages|prâkrit, langues pra|||Prakrit languages|prâkrit, langues
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500) pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
pus||ps|Pushto; Pashto|pachto pus||ps|Pushto; Pashto|pachto

View File

@@ -244,6 +244,7 @@ namespace Emby.Server.Implementations.Playlists
// Update the playlist in the repository // Update the playlist in the repository
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd]; playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
playlist.DateLastMediaAdded = DateTime.UtcNow;
await UpdatePlaylistInternal(playlist).ConfigureAwait(false); await UpdatePlaylistInternal(playlist).ConfigureAwait(false);

View File

@@ -156,6 +156,11 @@ namespace Emby.Server.Implementations.Updates
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest); _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
return Array.Empty<PackageInfo>(); return Array.Empty<PackageInfo>();
} }
catch (NotSupportedException ex)
{
_logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
catch (HttpRequestException ex) catch (HttpRequestException ex)
{ {
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest); _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
@@ -223,15 +228,14 @@ namespace Emby.Server.Implementations.Updates
Guid id = default, Guid id = default,
Version? specificVersion = null) Version? specificVersion = null)
{ {
if (name is not null)
{
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (!id.IsEmpty()) if (!id.IsEmpty())
{ {
availablePackages = availablePackages.Where(x => x.Id.Equals(id)); availablePackages = availablePackages.Where(x => x.Id.Equals(id));
} }
else if (name is not null)
{
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (specificVersion is not null) if (specificVersion is not null)
{ {

View File

@@ -1625,8 +1625,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions; var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT if (state.VideoStream is not null && state.IsOutputVideo)
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont"; {
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
}
segmentFormat = "fmp4" + outputFmp4HeaderArg; segmentFormat = "fmp4" + outputFmp4HeaderArg;
} }
@@ -1836,8 +1839,9 @@ public class DynamicHlsController : BaseJellyfinApiController
{ {
if (isActualOutputVideoCodecHevc) if (isActualOutputVideoCodecHevc)
{ {
// Prefer dvh1 to dvhe // Use hvc1 for 8.4. This is what Dolby uses for its official sample streams. Tagging with dvh1 would break some players with strict tag checking like Apple Safari.
args += " -tag:v:0 dvh1 -strict -2"; var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1";
args += $" -tag:v:0 {codecTag} -strict -2";
} }
else if (isActualOutputVideoCodecAv1) else if (isActualOutputVideoCodecAv1)
{ {

View File

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

View File

@@ -23,6 +23,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
@@ -700,7 +701,18 @@ public class LibraryController : BaseJellyfinApiController
// Quotes are valid in linux. They'll possibly cause issues here. // Quotes are valid in linux. They'll possibly cause issues here.
var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal); var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true); var filePath = item.Path;
if (item.IsFileProtocol)
{
// PhysicalFile does not work well with symlinks at the moment.
var resolved = FileSystemHelper.ResolveLinkTarget(filePath, returnFinalTarget: true);
if (resolved is not null && resolved.Exists)
{
filePath = resolved.FullName;
}
}
return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), filename, true);
} }
/// <summary> /// <summary>

View File

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

View File

@@ -86,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController
[FromRoute, Required] int index, [FromRoute, Required] int index,
[FromQuery] Guid? mediaSourceId) [FromQuery] Guid? mediaSourceId)
{ {
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId()); var item = _libraryManager.GetItemById<BaseItem>(mediaSourceId ?? itemId, User.GetUserId());
if (item is null) if (item is null)
{ {
return NotFound(); return NotFound();

View File

@@ -1,4 +1,8 @@
using System;
using System.Net.Mime; using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
namespace Jellyfin.Api.Formatters; namespace Jellyfin.Api.Formatters;
@@ -6,7 +10,7 @@ namespace Jellyfin.Api.Formatters;
/// <summary> /// <summary>
/// Xml output formatter. /// Xml output formatter.
/// </summary> /// </summary>
public sealed class XmlOutputFormatter : StringOutputFormatter public sealed class XmlOutputFormatter : TextOutputFormatter
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class. /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
@@ -15,5 +19,24 @@ public sealed class XmlOutputFormatter : StringOutputFormatter
{ {
SupportedMediaTypes.Clear(); SupportedMediaTypes.Clear();
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml); SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
/// <inheritdoc />
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(selectedEncoding);
var valueAsString = context.Object?.ToString();
if (string.IsNullOrEmpty(valueAsString))
{
return;
}
var response = context.HttpContext.Response;
await response.WriteAsync(valueAsString, selectedEncoding).ConfigureAwait(false);
} }
} }

View File

@@ -154,7 +154,7 @@ public class DynamicHlsHelper
// from universal audio service, need to override the AudioCodec when the actual request differs from original query // from universal audio service, need to override the AudioCodec when the actual request differs from original query
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase)) if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
{ {
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString()); var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
newQuery["AudioCodec"] = state.OutputAudioCodec; newQuery["AudioCodec"] = state.OutputAudioCodec;
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery); queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
} }
@@ -173,10 +173,21 @@ public class DynamicHlsHelper
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
} }
// Main stream // Video rotation metadata is only supported in fMP4 remuxing
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; if (state.VideoStream is not null
&& state.VideoRequest is not null
&& (state.VideoStream?.Rotation ?? 0) != 0
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
{
queryString += "&AllowVideoStreamCopy=false";
}
playlistUrl += queryString; // Main stream
var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
var playlistUrl = baseUrl + queryString;
var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
var subtitleStreams = state.MediaSource var subtitleStreams = state.MediaSource
.MediaStreams .MediaStreams
@@ -198,37 +209,36 @@ public class DynamicHlsHelper
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
} }
// Video rotation metadata is only supported in fMP4 remuxing
if (state.VideoStream is not null
&& state.VideoRequest is not null
&& (state.VideoStream?.Rotation ?? 0) != 0
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
{
playlistUrl += "&AllowVideoStreamCopy=false";
}
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (state.VideoStream is not null && state.VideoRequest is not null) if (state.VideoStream is not null && state.VideoRequest is not null)
{ {
var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// Provide SDR HEVC entrance for backward compatibility. // Provide AV1 and HEVC SDR entrances for backward compatibility.
if (encodingOptions.AllowHevcEncoding foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
&& !encodingOptions.AllowAv1Encoding
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRange == VideoRange.HDR
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0) && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
&& string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
if (isEncodingAllowed
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRange == VideoRange.HDR)
{ {
// Force HEVC Main Profile and disable video stream copy. // Force AV1 and HEVC Main Profile and disable video stream copy.
state.OutputVideoCodec = "hevc"; state.OutputVideoCodec = sdrVideoCodec;
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
sdrVideoUrl += "&AllowVideoStreamCopy=false"; var sdrPlaylistQuery = playlistQuery;
sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range. // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup); AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
@@ -238,12 +248,30 @@ public class DynamicHlsHelper
} }
} }
// Provide H.264 SDR entrance for backward compatibility.
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRange == VideoRange.HDR)
{
// Force H.264 and disable video stream copy.
state.OutputVideoCodec = "h264";
var sdrPlaylistQuery = playlistQuery;
sdrPlaylistQuery["VideoCodec"] = "h264";
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
// Restore the video codec
state.OutputVideoCodec = "copy";
}
// Provide Level 5.0 entrance for backward compatibility. // Provide Level 5.0 entrance for backward compatibility.
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video, // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
// but in fact it is capable of playing videos up to Level 6.1. // but in fact it is capable of playing videos up to Level 6.1.
if (encodingOptions.AllowHevcEncoding if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !encodingOptions.AllowAv1Encoding
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.Level.HasValue && state.VideoStream.Level.HasValue
&& state.VideoStream.Level > 150 && state.VideoStream.Level > 150
&& state.VideoStream.VideoRange == VideoRange.SDR && state.VideoStream.VideoRange == VideoRange.SDR
@@ -273,12 +301,15 @@ public class DynamicHlsHelper
var variation = GetBitrateVariation(totalBitrate); var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation; var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); var variantQuery = playlistQuery;
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2; variation *= 2;
newBitrate = totalBitrate - variation; newBitrate = totalBitrate - variation;
variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
} }
@@ -863,23 +894,6 @@ public class DynamicHlsHelper
return variation; return variation;
} }
private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase);
}
private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
{
string profileStr = codec + "-profile=";
return url.Replace(
profileStr + oldValue,
profileStr + newValue,
StringComparison.OrdinalIgnoreCase);
}
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue) private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
{ {
var oldPlaylist = playlist.ToString(); var oldPlaylist = playlist.ToString();

View File

@@ -159,6 +159,13 @@ public static class StreamingHelpers
string? containerInternal = Path.GetExtension(state.RequestedUrl); string? containerInternal = Path.GetExtension(state.RequestedUrl);
if (string.IsNullOrEmpty(containerInternal)
&& (!string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)
|| (mediaSource != null && mediaSource.IsInfiniteStream)))
{
containerInternal = ".ts";
}
if (!string.IsNullOrEmpty(streamingRequest.Container)) if (!string.IsNullOrEmpty(streamingRequest.Container))
{ {
containerInternal = streamingRequest.Container; containerInternal = streamingRequest.Container;

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.11.0</VersionPrefix> <VersionPrefix>10.11.6</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

@@ -128,7 +128,8 @@ public class BackupService : IBackupService
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName))); var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal) if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)) || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|| Path.EndsInDirectorySeparator(item.FullName))
{ {
continue; continue;
} }
@@ -199,7 +200,7 @@ public class BackupService : IBackupService
var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json"))); var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
if (zipEntry is null) if (zipEntry is null)
{ {
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name); _logger.LogInformation("No backup of expected table {Table} is present in backup, continuing anyway", entityType.Type.Name);
continue; continue;
} }
@@ -223,7 +224,7 @@ public class BackupService : IBackupService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item); _logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item);
} }
} }
@@ -233,11 +234,11 @@ public class BackupService : IBackupService
_logger.LogInformation("Try restore Database"); _logger.LogInformation("Try restore Database");
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogInformation("Restored database."); _logger.LogInformation("Restored database");
} }
} }
_logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated); _logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated);
} }
} }
@@ -263,6 +264,8 @@ public class BackupService : IBackupService
Options = Map(backupOptions) Options = Map(backupOptions)
}; };
_logger.LogInformation("Running database optimization before backup");
await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false); await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
var backupFolder = Path.Combine(_applicationPaths.BackupPath); var backupFolder = Path.Combine(_applicationPaths.BackupPath);
@@ -281,130 +284,154 @@ public class BackupService : IBackupService
} }
var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip"); var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip");
_logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath);
var fileStream = File.OpenWrite(backupPath); try
await using (fileStream.ConfigureAwait(false))
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
{ {
_logger.LogInformation("Start backup process."); _logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var fileStream = File.OpenWrite(backupPath);
await using (dbContext.ConfigureAwait(false)) await using (fileStream.ConfigureAwait(false))
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
{ {
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; _logger.LogInformation("Starting backup process");
static IAsyncEnumerable<object> GetValues(IQueryable dbSet) var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{ {
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!; dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var enumerable = method.Invoke(dbSet, null)!;
return (IAsyncEnumerable<object>)enumerable;
}
// include the migration history as well static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
.. typeof(JellyfinDbContext)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
_logger.LogInformation("Begin Database backup");
foreach (var entityType in entityTypes)
{ {
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName); var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json"))); var enumerable = method.Invoke(dbSet, null)!;
var entities = 0; return (IAsyncEnumerable<object>)enumerable;
var zipEntryStream = zipEntry.Open(); }
await using (zipEntryStream.ConfigureAwait(false))
// include the migration history as well
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes =
[
.. typeof(JellyfinDbContext)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
_logger.LogInformation("Begin Database backup");
foreach (var entityType in entityTypes)
{ {
var jsonSerializer = new Utf8JsonWriter(zipEntryStream); _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
await using (jsonSerializer.ConfigureAwait(false)) var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
var entities = 0;
var zipEntryStream = zipEntry.Open();
await using (zipEntryStream.ConfigureAwait(false))
{ {
jsonSerializer.WriteStartArray(); var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
await using (jsonSerializer.ConfigureAwait(false))
var set = entityType.ValueFactory().ConfigureAwait(false);
await foreach (var item in set.ConfigureAwait(false))
{ {
entities++; jsonSerializer.WriteStartArray();
try
var set = entityType.ValueFactory().ConfigureAwait(false);
await foreach (var item in set.ConfigureAwait(false))
{ {
JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer); entities++;
} try
catch (Exception ex) {
{ using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings);
_logger.LogError(ex, "Could not load entity {Entity}", item); document.WriteTo(jsonSerializer);
throw; }
catch (Exception ex)
{
_logger.LogError(ex, "Could not load entity {Entity}", item);
throw;
}
} }
jsonSerializer.WriteEndArray();
} }
jsonSerializer.WriteEndArray();
} }
}
_logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities); _logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities);
}
} }
} }
}
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath); _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly) foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly))) .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
{
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
}
void CopyDirectory(string source, string target, string filter = "*")
{
if (!Directory.Exists(source))
{ {
return; zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
} }
_logger.LogInformation("Backup of folder {Table}", source); void CopyDirectory(string source, string target, string filter = "*")
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
{ {
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item)))); if (!Directory.Exists(source))
{
return;
}
_logger.LogInformation("Backup of folder {Table}", source);
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
{
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
}
}
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
if (backupOptions.Subtitles)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
}
if (backupOptions.Trickplay)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
}
if (backupOptions.Metadata)
{
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
}
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
await using (manifestStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
} }
} }
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users")); _logger.LogInformation("Backup created");
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks")); return Map(manifest, backupPath);
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
if (backupOptions.Subtitles)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
}
if (backupOptions.Trickplay)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
}
if (backupOptions.Metadata)
{
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
}
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
await using (manifestStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
}
} }
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
try
{
if (File.Exists(backupPath))
{
File.Delete(backupPath);
}
}
catch (Exception innerEx)
{
_logger.LogWarning(innerEx, "Unable to remove failed backup");
}
_logger.LogInformation("Backup created"); throw;
return Map(manifest, backupPath); }
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -422,7 +449,7 @@ public class BackupService : IBackupService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath); _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath);
return null; return null;
} }
@@ -459,7 +486,7 @@ public class BackupService : IBackupService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Could not load {BackupArchive} path.", item); _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item);
} }
} }

View File

@@ -275,6 +275,7 @@ public sealed class BaseItemRepository
} }
dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
result.StartIndex = filter.StartIndex ?? 0; result.StartIndex = filter.StartIndex ?? 0;
@@ -294,6 +295,7 @@ public sealed class BaseItemRepository
dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
} }
@@ -337,6 +339,8 @@ public sealed class BaseItemRepository
mainquery = ApplyGroupingFilter(context, mainquery, filter); mainquery = ApplyGroupingFilter(context, mainquery, filter);
mainquery = ApplyQueryPaging(mainquery, filter); mainquery = ApplyQueryPaging(mainquery, filter);
mainquery = ApplyNavigations(mainquery, filter);
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
} }
@@ -399,19 +403,32 @@ public sealed class BaseItemRepository
dbQuery = dbQuery.Distinct(); dbQuery = dbQuery.Distinct();
} }
dbQuery = ApplyOrder(dbQuery, filter); dbQuery = ApplyOrder(dbQuery, filter, context);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery; return dbQuery;
} }
private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{ {
dbQuery = dbQuery.Include(e => e.TrailerTypes) if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
.Include(e => e.Provider) {
.Include(e => e.LockedFields) dbQuery = dbQuery.Include(e => e.TrailerTypes);
.Include(e => e.UserData); }
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
{
dbQuery = dbQuery.Include(e => e.Provider);
}
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
{
dbQuery = dbQuery.Include(e => e.LockedFields);
}
if (filter.DtoOptions.EnableUserData)
{
dbQuery = dbQuery.Include(e => e.UserData);
}
if (filter.DtoOptions.EnableImages) if (filter.DtoOptions.EnableImages)
{ {
@@ -446,6 +463,7 @@ public sealed class BaseItemRepository
dbQuery = TranslateQuery(dbQuery, context, filter); dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery; return dbQuery;
} }
@@ -599,7 +617,6 @@ public sealed class BaseItemRepository
var ids = tuples.Select(f => f.Item.Id).ToArray(); var ids = tuples.Select(f => f.Item.Id).ToArray();
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray(); var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
foreach (var item in tuples) foreach (var item in tuples)
{ {
@@ -614,25 +631,25 @@ public sealed class BaseItemRepository
else else
{ {
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete(); context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
if (entity.Images is { Count: > 0 })
{
context.BaseItemImageInfos.AddRange(entity.Images);
}
if (entity.LockedFields is { Count: > 0 })
{
context.BaseItemMetadataFields.AddRange(entity.LockedFields);
}
context.BaseItems.Attach(entity).State = EntityState.Modified; context.BaseItems.Attach(entity).State = EntityState.Modified;
} }
} }
context.SaveChanges(); context.SaveChanges();
foreach (var item in newItems)
{
// reattach old userData entries
var userKeys = item.UserDataKey.ToArray();
var retentionDate = (DateTime?)null;
context.UserData
.Where(e => e.ItemId == PlaceholderId)
.Where(e => userKeys.Contains(e.CustomDataKey))
.ExecuteUpdate(e => e
.SetProperty(f => f.ItemId, item.Item.Id)
.SetProperty(f => f.RetentionDate, retentionDate));
}
var itemValueMaps = tuples var itemValueMaps = tuples
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
.ToArray(); .ToArray();
@@ -728,6 +745,29 @@ public sealed class BaseItemRepository
transaction.Commit(); transaction.Commit();
} }
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(item);
cancellationToken.ThrowIfCancellationRequested();
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var userKeys = item.GetUserDataKeys().ToArray();
var retentionDate = (DateTime?)null;
await dbContext.UserData
.Where(e => e.ItemId == PlaceholderId)
.Where(e => userKeys.Contains(e.CustomDataKey))
.ExecuteUpdateAsync(
e => e
.SetProperty(f => f.ItemId, item.Id)
.SetProperty(f => f.RetentionDate, retentionDate),
cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc /> /// <inheritdoc />
public BaseItemDto? RetrieveItem(Guid id) public BaseItemDto? RetrieveItem(Guid id)
{ {
@@ -835,7 +875,7 @@ public sealed class BaseItemRepository
} }
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray(); dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? []; dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
dto.Studios = entity.Studios?.Split('|') ?? []; dto.Studios = entity.Studios?.Split('|') ?? [];
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|'); dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
@@ -997,7 +1037,7 @@ public sealed class BaseItemRepository
} }
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null; entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null; entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null; entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null; entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
@@ -1245,7 +1285,7 @@ public sealed class BaseItemRepository
.AsSingleQuery() .AsSingleQuery()
.Where(e => masterQuery.Contains(e.Id)); .Where(e => masterQuery.Contains(e.Id));
query = ApplyOrder(query, filter); query = ApplyOrder(query, filter, context);
var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
if (filter.EnableTotalRecordCount) if (filter.EnableTotalRecordCount)
@@ -1511,51 +1551,58 @@ public sealed class BaseItemRepository
|| query.IncludeItemTypes.Contains(BaseItemKind.Season); || query.IncludeItemTypes.Contains(BaseItemKind.Season);
} }
private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter) private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context)
{ {
var orderBy = filter.OrderBy; var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
if (hasSearch) if (hasSearch)
{ {
orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy]; orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
} }
else if (orderBy.Count == 0) else if (orderBy.Length == 0)
{ {
return query.OrderBy(e => e.SortName); return query.OrderBy(e => e.SortName);
} }
IOrderedQueryable<BaseItemEntity>? orderedQuery = null; IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
// When searching, prioritize by match quality: exact match > prefix match > contains
if (hasSearch)
{
orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
}
var firstOrdering = orderBy.FirstOrDefault(); var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default) if (firstOrdering != default)
{ {
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter); var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
if (firstOrdering.SortOrder == SortOrder.Ascending) if (orderedQuery is null)
{ {
orderedQuery = query.OrderBy(expression); // No search relevance ordering, start fresh
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
? query.OrderBy(expression)
: query.OrderByDescending(expression);
} }
else else
{ {
orderedQuery = query.OrderByDescending(expression); // Search relevance ordering already applied, chain with ThenBy
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
? orderedQuery.ThenBy(expression)
: orderedQuery.ThenByDescending(expression);
} }
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName) if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
{ {
if (firstOrdering.SortOrder is SortOrder.Ascending) orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
{ ? orderedQuery.ThenBy(e => e.Name)
orderedQuery = orderedQuery.ThenBy(e => e.Name); : orderedQuery.ThenByDescending(e => e.Name);
}
else
{
orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
}
} }
} }
foreach (var item in orderBy.Skip(1)) foreach (var item in orderBy.Skip(1))
{ {
var expression = OrderMapper.MapOrderByField(item.OrderBy, filter); var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
if (item.SortOrder == SortOrder.Ascending) if (item.SortOrder == SortOrder.Ascending)
{ {
orderedQuery = orderedQuery!.ThenBy(expression); orderedQuery = orderedQuery!.ThenBy(expression);
@@ -1637,19 +1684,18 @@ public sealed class BaseItemRepository
var tags = filter.Tags.ToList(); var tags = filter.Tags.ToList();
var excludeTags = filter.ExcludeTags.ToList(); var excludeTags = filter.ExcludeTags.ToList();
if (filter.IsMovie == true) if (filter.IsMovie.HasValue)
{ {
if (filter.IncludeItemTypes.Length == 0 var shouldIncludeAllMovieTypes = filter.IsMovie.Value
|| filter.IncludeItemTypes.Contains(BaseItemKind.Movie) && (filter.IncludeItemTypes.Length == 0
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
if (!shouldIncludeAllMovieTypes)
{ {
baseQuery = baseQuery.Where(e => e.IsMovie); baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
} }
} }
else if (filter.IsMovie.HasValue)
{
baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
}
if (filter.IsSeries.HasValue) if (filter.IsSeries.HasValue)
{ {
@@ -1694,15 +1740,16 @@ public sealed class BaseItemRepository
if (!string.IsNullOrEmpty(filter.SearchTerm)) if (!string.IsNullOrEmpty(filter.SearchTerm))
{ {
var searchTerm = filter.SearchTerm.ToLower(); var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
if (SearchWildcardTerms.Any(f => searchTerm.Contains(f))) var originalSearchTerm = filter.SearchTerm.ToLower();
if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
{ {
searchTerm = $"%{searchTerm.Trim('%')}%"; cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!.ToLower(), searchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), searchTerm))); baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), originalSearchTerm)));
} }
else else
{ {
baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm))); baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm)));
} }
} }
@@ -1756,7 +1803,8 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.Path)) if (!string.IsNullOrWhiteSpace(filter.Path))
{ {
baseQuery = baseQuery.Where(e => e.Path == filter.Path); var pathToQuery = GetPathToSave(filter.Path);
baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
} }
if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
@@ -1913,8 +1961,15 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.Name)) if (!string.IsNullOrWhiteSpace(filter.Name))
{ {
var cleanName = GetCleanValue(filter.Name); if (filter.UseRawName == true)
baseQuery = baseQuery.Where(e => e.CleanName == cleanName); {
baseQuery = baseQuery.Where(e => e.Name == filter.Name);
}
else
{
var cleanName = GetCleanValue(filter.Name);
baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
}
} }
// These are the same, for now // These are the same, for now
@@ -1936,19 +1991,20 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
{ {
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith)); var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
} }
if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
{ {
// i hate this var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]); baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
} }
if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
{ {
// i hate this var lessThanLower = filter.NameLessThan.ToLowerInvariant();
baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]); baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
} }
if (filter.ImageTypes.Length > 0) if (filter.ImageTypes.Length > 0)
@@ -2046,7 +2102,7 @@ public sealed class BaseItemRepository
if (filter.ExcludeArtistIds.Length > 0) if (filter.ExcludeArtistIds.Length > 0)
{ {
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true); baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
} }
if (filter.GenreIds.Count > 0) if (filter.GenreIds.Count > 0)
@@ -2353,17 +2409,23 @@ public sealed class BaseItemRepository
if (filter.HasImdbId.HasValue) if (filter.HasImdbId.HasValue)
{ {
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); baseQuery = filter.HasImdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
} }
if (filter.HasTmdbId.HasValue) if (filter.HasTmdbId.HasValue)
{ {
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); baseQuery = filter.HasTmdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
} }
if (filter.HasTvdbId.HasValue) if (filter.HasTvdbId.HasValue)
{ {
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); baseQuery = filter.HasTvdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
} }
var queryTopParentIds = filter.TopParentIds; var queryTopParentIds = filter.TopParentIds;
@@ -2401,40 +2463,24 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0) if (filter.ExcludeInheritedTags.Length > 0)
{ {
baseQuery = baseQuery var excludedTags = filter.ExcludeInheritedTags;
.Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags) baseQuery = baseQuery.Where(e =>
.Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))); !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
&& (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))));
} }
if (filter.IncludeInheritedTags.Length > 0) if (filter.IncludeInheritedTags.Length > 0)
{ {
// Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. var includeTags = filter.IncludeInheritedTags;
// In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) baseQuery = baseQuery.Where(e =>
{ e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
baseQuery = baseQuery
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
||
(e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags))
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
}
// A playlist should be accessible to its owner regardless of allowed tags. // For seasons and episodes, we also need to check the parent series' tags.
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)))
{
baseQuery = baseQuery // A playlist should be accessible to its owner regardless of allowed tags
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
// d ^^ this is stupid it hate this.
}
else
{
baseQuery = baseQuery
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
} }
if (filter.SeriesStatuses.Length > 0) if (filter.SeriesStatuses.Length > 0)
@@ -2588,6 +2634,21 @@ public sealed class BaseItemRepository
.Where(e => artistNames.Contains(e.Name)) .Where(e => artistNames.Contains(e.Name))
.ToArray(); .ToArray();
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray()); var lookup = artists
.GroupBy(e => e.Name!)
.ToDictionary(
g => g.Key,
g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
foreach (var name in artistNames)
{
if (lookup.TryGetValue(name, out var artistArray))
{
result[name] = artistArray;
}
}
return result;
} }
} }

View File

@@ -1,8 +1,12 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System; using System;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -18,40 +22,77 @@ public static class OrderMapper
/// </summary> /// </summary>
/// <param name="sortBy">Item property to sort by.</param> /// <param name="sortBy">Item property to sort by.</param>
/// <param name="query">Context Query.</param> /// <param name="query">Context Query.</param>
/// <param name="jellyfinDbContext">Context.</param>
/// <returns>Func to be executed later for sorting query.</returns> /// <returns>Func to be executed later for sorting query.</returns>
public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query, JellyfinDbContext jellyfinDbContext)
{ {
return sortBy switch return (sortBy, query.User) switch
{ {
ItemSortBy.AirTime => e => e.SortName, // TODO (ItemSortBy.AirTime, _) => e => e.SortName, // TODO
ItemSortBy.Runtime => e => e.RunTimeTicks, (ItemSortBy.Runtime, _) => e => e.RunTimeTicks,
ItemSortBy.Random => e => EF.Functions.Random(), (ItemSortBy.Random, _) => e => EF.Functions.Random(),
ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate, (ItemSortBy.DatePlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate,
ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount, (ItemSortBy.PlayCount, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount,
ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite, (ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite,
ItemSortBy.IsFolder => e => e.IsFolder, (ItemSortBy.IsFolder, _) => e => e.IsFolder,
ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, (ItemSortBy.IsPlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, (ItemSortBy.IsUnplayed, _) => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, (ItemSortBy.DateLastContentAdded, _) => e => e.DateLastMediaAdded,
ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), (ItemSortBy.Artist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), (ItemSortBy.AlbumArtist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), (ItemSortBy.Studio, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, (ItemSortBy.OfficialRating, _) => e => e.InheritedParentalRatingValue,
// ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", (ItemSortBy.SeriesSortName, _) => e => e.SeriesName,
ItemSortBy.SeriesSortName => e => e.SeriesName, (ItemSortBy.Album, _) => e => e.Album,
(ItemSortBy.DateCreated, _) => e => e.DateCreated,
(ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
(ItemSortBy.StartDate, _) => e => e.StartDate,
(ItemSortBy.Name, _) => e => e.CleanName,
(ItemSortBy.CommunityRating, _) => e => e.CommunityRating,
(ItemSortBy.ProductionYear, _) => e => e.ProductionYear,
(ItemSortBy.CriticRating, _) => e => e.CriticRating,
(ItemSortBy.VideoBitRate, _) => e => e.TotalBitrate,
(ItemSortBy.ParentIndexNumber, _) => e => e.ParentIndexNumber,
(ItemSortBy.IndexNumber, _) => e => e.IndexNumber,
(ItemSortBy.SeriesDatePlayed, not null) => e =>
jellyfinDbContext.BaseItems
.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
.Join(jellyfinDbContext.UserData.Where(w => w.UserId == query.User.Id && w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
.Max(f => f),
(ItemSortBy.SeriesDatePlayed, null) => e => jellyfinDbContext.BaseItems.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
.Join(jellyfinDbContext.UserData.Where(w => w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
.Max(f => f),
// ItemSortBy.SeriesDatePlayed => e => jellyfinDbContext.UserData
// .Where(u => u.Item!.SeriesPresentationUniqueKey == e.PresentationUniqueKey && u.Played)
// .Max(f => f.LastPlayedDate),
// ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
ItemSortBy.Album => e => e.Album,
ItemSortBy.DateCreated => e => e.DateCreated,
ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
ItemSortBy.StartDate => e => e.StartDate,
ItemSortBy.Name => e => e.CleanName,
ItemSortBy.CommunityRating => e => e.CommunityRating,
ItemSortBy.ProductionYear => e => e.ProductionYear,
ItemSortBy.CriticRating => e => e.CriticRating,
ItemSortBy.VideoBitRate => e => e.TotalBitrate,
ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
ItemSortBy.IndexNumber => e => e.IndexNumber,
_ => e => e.SortName _ => e => e.SortName
}; };
} }
/// <summary>
/// Creates an expression to order search results by match quality.
/// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3).
/// </summary>
/// <param name="searchTerm">The search term to match against.</param>
/// <returns>An expression that returns an integer representing match quality (lower is better).</returns>
public static Expression<Func<BaseItemEntity, int>> MapSearchRelevanceOrder(string searchTerm)
{
var cleanSearchTerm = GetCleanValue(searchTerm);
var searchPrefix = cleanSearchTerm + " ";
return e =>
e.CleanName == cleanSearchTerm ? 0 :
e.CleanName!.StartsWith(searchPrefix) ? 1 :
e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3;
}
private static string GetCleanValue(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}
return value.RemoveDiacritics().ToLowerInvariant();
}
} }

View File

@@ -13,8 +13,7 @@ namespace Jellyfin.Server.Implementations.StorageHelpers;
public static class StorageHelper public static class StorageHelper
{ {
private const long TwoGigabyte = 2_147_483_647L; private const long TwoGigabyte = 2_147_483_647L;
private const long FiveHundredAndTwelveMegaByte = 536_870_911L; private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
private static readonly string[] _byteHumanizedSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
/// <summary> /// <summary>
/// Tests the available storage capacity on the jellyfin paths with estimated minimum values. /// Tests the available storage capacity on the jellyfin paths with estimated minimum values.
@@ -24,10 +23,8 @@ public static class StorageHelper
public static void TestCommonPathsForStorageCapacity(IApplicationPaths applicationPaths, ILogger logger) public static void TestCommonPathsForStorageCapacity(IApplicationPaths applicationPaths, ILogger logger)
{ {
TestDataDirectorySize(applicationPaths.DataPath, logger, TwoGigabyte); TestDataDirectorySize(applicationPaths.DataPath, logger, TwoGigabyte);
TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte);
TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte); TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte);
TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte); TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte);
TestDataDirectorySize(applicationPaths.TempDirectory, logger, TwoGigabyte);
} }
/// <summary> /// <summary>
@@ -77,7 +74,7 @@ public static class StorageHelper
var drive = new DriveInfo(path); var drive = new DriveInfo(path);
if (threshold != -1 && drive.AvailableFreeSpace < threshold) if (threshold != -1 && drive.AvailableFreeSpace < threshold)
{ {
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}."); throw new InvalidOperationException($"The path `{path}` has insufficient free space. Available: {HumanizeStorageSize(drive.AvailableFreeSpace)}, Required: {HumanizeStorageSize(threshold)}.");
} }
logger.LogInformation( logger.LogInformation(

View File

@@ -254,10 +254,10 @@ public class TrickplayManager : ITrickplayManager
} }
// We support video backdrops, but we should not generate trickplay images for them // We support video backdrops, but we should not generate trickplay images for them
var parentDirectory = Directory.GetParent(mediaPath); var parentDirectory = Directory.GetParent(video.Path);
if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase)) if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id); _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", video.Path, video.Id);
return; return;
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
@@ -92,33 +93,38 @@ namespace Jellyfin.Server.Implementations.Users
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork) public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork)
{ {
byte[] bytes = new byte[4];
RandomNumberGenerator.Fill(bytes);
string pin = BitConverter.ToString(bytes);
DateTime expireTime = DateTime.UtcNow.AddMinutes(30); DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
string filePath = _passwordResetFileBase + user.Id + ".json"; var usernameHash = enteredUsername.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
SerializablePasswordReset spr = new SerializablePasswordReset var pinFile = _passwordResetFileBase + usernameHash + ".json";
{
ExpirationDate = expireTime,
Pin = pin,
PinFile = filePath,
UserName = user.Username
};
FileStream fileStream = AsyncFile.Create(filePath); if (user is not null && isInNetwork)
await using (fileStream.ConfigureAwait(false))
{ {
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); byte[] bytes = new byte[4];
RandomNumberGenerator.Fill(bytes);
string pin = BitConverter.ToString(bytes);
SerializablePasswordReset spr = new SerializablePasswordReset
{
ExpirationDate = expireTime,
Pin = pin,
PinFile = pinFile,
UserName = user.Username
};
FileStream fileStream = AsyncFile.Create(pinFile);
await using (fileStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
}
} }
return new ForgotPasswordResult return new ForgotPasswordResult
{ {
Action = ForgotPasswordAction.PinCode, Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime, PinExpirationDate = expireTime,
PinFile = filePath PinFile = pinFile
}; };
} }

View File

@@ -149,7 +149,7 @@ namespace Jellyfin.Server.Implementations.Users
ThrowIfInvalidUsername(newName); ThrowIfInvalidUsername(newName);
if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)) if (user.Username.Equals(newName, StringComparison.Ordinal))
{ {
throw new ArgumentException("The new and old names must be different."); throw new ArgumentException("The new and old names must be different.");
} }
@@ -508,23 +508,18 @@ namespace Jellyfin.Server.Implementations.Users
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{ {
var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
var passwordResetProvider = GetPasswordResetProvider(user);
var result = await passwordResetProvider
.StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
.ConfigureAwait(false);
if (user is not null && isInNetwork) if (user is not null && isInNetwork)
{ {
var passwordResetProvider = GetPasswordResetProvider(user);
var result = await passwordResetProvider
.StartForgotPasswordProcess(user, isInNetwork)
.ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false);
return result;
} }
return new ForgotPasswordResult return result;
{
Action = ForgotPasswordAction.InNetworkRequired,
PinFile = string.Empty
};
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -760,8 +755,13 @@ namespace Jellyfin.Server.Implementations.Users
return GetAuthenticationProviders(user)[0]; return GetAuthenticationProviders(user)[0];
} }
private IPasswordResetProvider GetPasswordResetProvider(User user) private IPasswordResetProvider GetPasswordResetProvider(User? user)
{ {
if (user is null)
{
return _defaultPasswordResetProvider;
}
return GetPasswordResetProviders(user)[0]; return GetPasswordResetProviders(user)[0];
} }

View File

@@ -33,9 +33,11 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
@@ -259,7 +261,8 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter<FileRequestFilter>(); c.OperationFilter<FileRequestFilter>();
c.OperationFilter<ParameterObsoleteFilter>(); c.OperationFilter<ParameterObsoleteFilter>();
c.DocumentFilter<AdditionalModelFilter>(); c.DocumentFilter<AdditionalModelFilter>();
}); })
.Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
} }
private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement) private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)

View File

@@ -0,0 +1,89 @@
using System;
using System.Threading;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
/// <summary>
/// OpenApi provider with caching.
/// </summary>
internal sealed class CachingOpenApiProvider : ISwaggerProvider
{
private const string CacheKey = "openapi.json";
private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
private static readonly SemaphoreSlim _lock = new(1, 1);
private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
private readonly IMemoryCache _memoryCache;
private readonly SwaggerGenerator _swaggerGenerator;
private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions;
/// <summary>
/// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class.
/// </summary>
/// <param name="optionsAccessor">The options accessor.</param>
/// <param name="apiDescriptionsProvider">The api descriptions provider.</param>
/// <param name="schemaGenerator">The schema generator.</param>
/// <param name="memoryCache">The memory cache.</param>
public CachingOpenApiProvider(
IOptions<SwaggerGeneratorOptions> optionsAccessor,
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
ISchemaGenerator schemaGenerator,
IMemoryCache memoryCache)
{
_swaggerGeneratorOptions = optionsAccessor.Value;
_swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator);
_memoryCache = memoryCache;
}
/// <inheritdoc />
public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
{
if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
{
return AdjustDocument(openApiDocument, host, basePath);
}
var acquired = _lock.Wait(_lockTimeout);
try
{
if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
{
return AdjustDocument(openApiDocument, host, basePath);
}
if (!acquired)
{
throw new InvalidOperationException("OpenApi document is generating");
}
openApiDocument = _swaggerGenerator.GetSwagger(documentName);
_memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
return AdjustDocument(openApiDocument, host, basePath);
}
finally
{
if (acquired)
{
_lock.Release();
}
}
}
private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)
{
document.Servers = _swaggerGeneratorOptions.Servers.Count != 0
? _swaggerGeneratorOptions.Servers
: string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath)
? []
: [new OpenApiServer { Url = $"{host}{basePath}" }];
return document;
}
}

View File

@@ -1,151 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) .NET Foundation and Contributors
//
// All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Server.Infrastructure
{
/// <inheritdoc />
public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
{
/// <summary>
/// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
/// </summary>
/// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
{
}
/// <inheritdoc />
protected override FileMetadata GetFileInfo(string path)
{
var fileInfo = new FileInfo(path);
var length = fileInfo.Length;
// This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
length = RandomAccess.GetLength(fileHandle);
}
return new FileMetadata
{
Exists = fileInfo.Exists,
Length = length,
LastModified = fileInfo.LastWriteTimeUtc
};
}
/// <inheritdoc />
protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(result);
if (range is not null && rangeLength == 0)
{
return;
}
// It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
if (!IsSymLink(result.FileName))
{
await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false);
return;
}
var response = context.HttpContext.Response;
if (range is not null)
{
await SendFileAsync(
result.FileName,
response,
offset: range.From ?? 0L,
count: rangeLength).ConfigureAwait(false);
return;
}
await SendFileAsync(
result.FileName,
response,
offset: 0,
count: null).ConfigureAwait(false);
}
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
{
var fileInfo = GetFileInfo(filePath);
if (offset < 0 || offset > fileInfo.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if (count.HasValue
&& (count.Value < 0 || count.Value > fileInfo.Length - offset))
{
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
}
// Copied from SendFileFallback.SendFileAsync
const int BufferSize = 1024 * 16;
var useRequestAborted = !cancellationToken.CanBeCanceled;
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: BufferSize,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
try
{
localCancel.ThrowIfCancellationRequested();
fileStream.Seek(offset, SeekOrigin.Begin);
await StreamCopyOperation
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
.ConfigureAwait(true);
}
catch (OperationCanceledException) when (useRequestAborted)
{
}
}
}
private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
}
}

View File

@@ -78,7 +78,7 @@
<None Update="wwwroot\api-docs\swagger\custom.css"> <None Update="wwwroot\api-docs\swagger\custom.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="wwwroot\api-docs\banner-dark.svg"> <None Update="wwwroot\api-docs\jellyfin.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="ServerSetupApp/index.mstemplate.html"> <None Update="ServerSetupApp/index.mstemplate.html">

View File

@@ -55,9 +55,25 @@ namespace Jellyfin.Server.Migrations.Routines
}; };
var dataPath = _paths.DataPath; var dataPath = _paths.DataPath;
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) var activityLogPath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(activityLogPath))
{
_logger.LogWarning("{ActivityLogDb} doesn't exist, nothing to migrate", activityLogPath);
return;
}
using (var connection = new SqliteConnection($"Filename={activityLogPath}"))
{ {
connection.Open(); connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ActivityLog';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
return;
}
}
using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}"); using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
userDbConnection.Open(); userDbConnection.Open();

View File

@@ -50,9 +50,28 @@ namespace Jellyfin.Server.Migrations.Routines
public void Perform() public void Perform()
{ {
var dataPath = _appPaths.DataPath; var dataPath = _appPaths.DataPath;
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) var dbFilePath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(dbFilePath))
{
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
return;
}
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
{ {
connection.Open(); connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Tokens';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'Tokens' doesn't exist in {Path}, nothing to migrate", dbFilePath);
return;
}
}
using var dbContext = _dbProvider.CreateDbContext(); using var dbContext = _dbProvider.CreateDbContext();
var authenticatedDevices = connection.Query("SELECT * FROM Tokens"); var authenticatedDevices = connection.Query("SELECT * FROM Tokens");

View File

@@ -78,9 +78,27 @@ namespace Jellyfin.Server.Migrations.Routines
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
if (!File.Exists(dbFilePath))
{
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
return;
}
using (var connection = new SqliteConnection($"Filename={dbFilePath}")) using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
{ {
connection.Open(); connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='userdisplaypreferences';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'userdisplaypreferences' doesn't exist in {Path}, nothing to migrate", dbFilePath);
return;
}
}
using var dbContext = _provider.CreateDbContext(); using var dbContext = _provider.CreateDbContext();
var results = connection.Query("SELECT * FROM userdisplaypreferences"); var results = connection.Query("SELECT * FROM userdisplaypreferences");

View File

@@ -122,6 +122,16 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
{ {
lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath); lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
} }
catch (ArgumentOutOfRangeException e)
{
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
return null;
}
catch (UnauthorizedAccessException e)
{
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
return null;
}
catch (IOException e) catch (IOException e)
{ {
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message); _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
@@ -135,14 +145,21 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
return Path.Join(keyframeCachePath, prefix, filename); return Path.Join(keyframeCachePath, prefix, filename);
} }
private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult) private bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
{ {
if (File.Exists(cachePath)) if (File.Exists(cachePath))
{ {
var bytes = File.ReadAllBytes(cachePath); try
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions); {
var bytes = File.ReadAllBytes(cachePath);
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
return cachedResult is not null; return cachedResult is not null;
}
catch (JsonException jsonException)
{
_logger.LogWarning(jsonException, "Failed to read {Path}", cachePath);
}
} }
cachedResult = null; cachedResult = null;

View File

@@ -383,8 +383,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}); });
} }
baseItemIds.Clear();
foreach (var item in peopleCache) foreach (var item in peopleCache)
{ {
operation.JellyfinDbContext.Peoples.Add(item.Value.Person); operation.JellyfinDbContext.Peoples.Add(item.Value.Person);

View File

@@ -57,11 +57,28 @@ public class MigrateUserDb : IMigrationRoutine
public void Perform() public void Perform()
{ {
var dataPath = _paths.DataPath; var dataPath = _paths.DataPath;
var userDbPath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(userDbPath))
{
_logger.LogWarning("{UserDbPath} doesn't exist, nothing to migrate", userDbPath);
return;
}
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) using (var connection = new SqliteConnection($"Filename={userDbPath}"))
{ {
connection.Open(); connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='LocalUsersv2';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
return;
}
}
using 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

@@ -224,6 +224,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
return null; return null;
} }
catch (UnauthorizedAccessException e)
{
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
return null;
}
catch (ArgumentOutOfRangeException e)
{
_logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
return null;
}
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
} }
@@ -263,6 +275,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
{ {
date = File.GetLastWriteTimeUtc(path); date = File.GetLastWriteTimeUtc(path);
} }
catch (ArgumentOutOfRangeException e)
{
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
return null;
}
catch (UnauthorizedAccessException e)
{
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
return null;
}
catch (IOException e) catch (IOException e)
{ {
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message); _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);

View File

@@ -184,6 +184,12 @@ namespace Jellyfin.Server
.AddSingleton<IServiceCollection>(e)) .AddSingleton<IServiceCollection>(e))
.Build(); .Build();
/*
* Initialize the transcode path marker so we avoid starting Jellyfin in a broken state.
* This should really be a part of IApplicationPaths but this path is configured differently.
*/
_ = appHost.ConfigurationManager.GetTranscodePath();
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = _jellyfinHost.Services; appHost.ServiceProvider = _jellyfinHost.Services;
PrepareDatabaseProvider(appHost.ServiceProvider); PrepareDatabaseProvider(appHost.ServiceProvider);

View File

@@ -250,6 +250,7 @@ public sealed class SetupServer : IDisposable
{ "isInReportingMode", _isUnhealthy }, { "isInReportingMode", _isUnhealthy },
{ "retryValue", retryAfterValue }, { "retryValue", retryAfterValue },
{ "logs", startupLogEntries }, { "logs", startupLogEntries },
{ "networkManagerReady", networkManager is not null },
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) } { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
}, },
new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions)) new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))

View File

@@ -213,7 +213,12 @@
</ol> </ol>
</div> </div>
{{#ELSE}} {{#ELSE}}
{{#IF networkManagerReady}}
<p>Please visit this page from your local network to view detailed startup logs.</p> <p>Please visit this page from your local network to view detailed startup logs.</p>
{{#ELSE}}
<p>Initializing network settings. Please wait.</p>
{{/ELSE}}
{{/IF}}
{{/ELSE}} {{/ELSE}}
{{/IF}} {{/IF}}
</div> </div>

View File

@@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs;
using Jellyfin.Server.Extensions; using Jellyfin.Server.Extensions;
using Jellyfin.Server.HealthChecks; using Jellyfin.Server.HealthChecks;
using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Infrastructure;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using MediaBrowser.XbmcMetadata; using MediaBrowser.XbmcMetadata;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -69,8 +66,6 @@ namespace Jellyfin.Server
options.HttpsPort = _serverApplicationHost.HttpsPort; options.HttpsPort = _serverApplicationHost.HttpsPort;
}); });
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration); services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
services.AddJellyfinApiSwagger(); services.AddJellyfinApiSwagger();

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ***** BEGIN LICENSE BLOCK *****
- Part of the Jellyfin project (https://jellyfin.media)
-
- All copyright belongs to the Jellyfin contributors; a full list can
- be found in the file CONTRIBUTORS.md
-
- This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/.
- ***** END LICENSE BLOCK ***** -->
<svg id="banner-dark" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1536 512">
<defs>
<linearGradient id="linear-gradient" x1="110.25" y1="213.3" x2="496.14" y2="436.09" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#aa5cc3"/>
<stop offset="1" stop-color="#00a4dc"/>
</linearGradient>
</defs>
<title>banner-dark</title>
<g id="banner-dark">
<g id="banner-dark-icon">
<path id="inner-shape" d="M261.42,201.62c-20.44,0-86.24,119.29-76.2,139.43s142.48,19.92,152.4,0S281.86,201.63,261.42,201.62Z" fill="url(#linear-gradient)"/>
<path id="outer-shape" d="M261.42,23.3C199.83,23.3,1.57,382.73,31.8,443.43s429.34,60,459.24,0S323,23.3,261.42,23.3ZM411.9,390.76c-19.59,39.33-281.08,39.77-300.9,0S221.1,115.48,261.45,115.48,431.49,351.42,411.9,390.76Z" fill="url(#linear-gradient)"/>
</g>
<g id="jellyfin-light-outlines" style="isolation:isolate" transform="translate(43.8)">
<path d="M556.64,350.75a67,67,0,0,1-22.87-27.47,8.91,8.91,0,0,1-1.49-4.75,7.42,7.42,0,0,1,2.83-5.94,9.25,9.25,0,0,1,6.09-2.38c3.16,0,5.94,1.69,8.31,5.05a48.09,48.09,0,0,0,16.34,20.34,40.59,40.59,0,0,0,24,7.58q20.51,0,33.27-12.62t12.77-33.12V159a8.44,8.44,0,0,1,2.67-6.39,9.56,9.56,0,0,1,6.83-2.52,9,9,0,0,1,6.68,2.52,8.7,8.7,0,0,1,2.53,6.39v138.4a64.7,64.7,0,0,1-8.32,32.67,59,59,0,0,1-23,22.72Q608.62,361,589.9,361A57.21,57.21,0,0,1,556.64,350.75Z" fill="#fff"/>
<path d="M831.66,279.47a8.77,8.77,0,0,1-6.24,2.53H713.16q0,17.82,7.27,31.92a54.91,54.91,0,0,0,20.79,22.28q13.51,8.18,31.93,8.17a54,54,0,0,0,25.54-5.94,52.7,52.7,0,0,0,18.12-15.15,10,10,0,0,1,6.24-2.67,8.14,8.14,0,0,1,7.72,7.72,8.81,8.81,0,0,1-3,6.24,74.7,74.7,0,0,1-23.91,19A65.56,65.56,0,0,1,773.45,361q-22.87,0-40.4-9.8a69.51,69.51,0,0,1-27.32-27.48q-9.79-17.66-9.8-40.83,0-24.36,9.65-42.62t25.69-27.92a65.2,65.2,0,0,1,34.16-9.65A70,70,0,0,1,798.84,211a65.78,65.78,0,0,1,25.39,24.36q9.81,16,10.1,38A8.07,8.07,0,0,1,831.66,279.47ZM733.5,231.8Q718.8,243.68,714.64,266H815.92v-2.38A46.91,46.91,0,0,0,807,240.27a48.47,48.47,0,0,0-18.56-15.15,54,54,0,0,0-23-5.2Q748.2,219.92,733.5,231.8Z" fill="#fff"/>
<path d="M888.24,355.5a8.92,8.92,0,0,1-15.3-6.38v-202a8.91,8.91,0,1,1,17.82,0v202A8.65,8.65,0,0,1,888.24,355.5Z" fill="#fff"/>
<path d="M956.55,355.5a8.92,8.92,0,0,1-15.3-6.38v-202a8.91,8.91,0,1,1,17.82,0v202A8.65,8.65,0,0,1,956.55,355.5Z" fill="#fff"/>
<path d="M1122.86,206.11a8.7,8.7,0,0,1,2.53,6.39v131q0,23.44-9.21,40.09a61.58,61.58,0,0,1-25.54,25.25q-16.34,8.61-36.83,8.61a96.73,96.73,0,0,1-23.31-2.68,61.72,61.72,0,0,1-18-7.12q-6.24-3.87-6.24-8.62a17.94,17.94,0,0,1,.6-3,8.06,8.06,0,0,1,3-4.45,7.49,7.49,0,0,1,4.45-1.49,7.91,7.91,0,0,1,3.56.89q19,10.39,36.24,10.4,24.65,0,39.06-15.44t14.4-42.18V333.38a54.37,54.37,0,0,1-21.38,20,62.55,62.55,0,0,1-30.3,7.58q-25.83,0-39.2-15.45t-13.37-41.87V212.5a8.91,8.91,0,1,1,17.82,0V301q0,21.39,9.36,32.38t29.25,11a48,48,0,0,0,23.32-6.09,49.88,49.88,0,0,0,17.82-16,37.44,37.44,0,0,0,6.68-21.24V212.5a9,9,0,0,1,15.29-6.39Z" fill="#fff"/>
<path d="M1210.18,161.41q-5.21,6.24-5.2,17.23v30.59h33.27a8.19,8.19,0,0,1,5.79,2.38,8.26,8.26,0,0,1,0,11.88,8.22,8.22,0,0,1-5.79,2.37H1205V349.12a8.91,8.91,0,1,1-17.82,0V225.86h-21.68a7.83,7.83,0,0,1-5.94-2.52,8.21,8.21,0,0,1-2.37-5.79,8,8,0,0,1,2.37-6.09,8.33,8.33,0,0,1,5.94-2.23h21.68V178.64q0-18.7,10.84-29t29-10.24a46.1,46.1,0,0,1,15.45,2.52q7.13,2.53,7.12,8.17a8.07,8.07,0,0,1-2.37,5.94,7.37,7.37,0,0,1-5.35,2.37,18.81,18.81,0,0,1-6.53-1.48,42,42,0,0,0-10.4-1.78Q1215.37,155.18,1210.18,161.41ZM1276,180.87c-2.19-1.88-3.27-4.61-3.27-8.17v-3q0-5.34,3.41-8.17t9.36-2.82q11.88,0,11.88,11v3c0,3.56-1,6.29-3.12,8.17s-5.1,2.82-9.06,2.82S1278.14,182.75,1276,180.87Zm15.59,174.63a8.92,8.92,0,0,1-15.3-6.38V212.5a8.91,8.91,0,1,1,17.82,0V349.12A8.65,8.65,0,0,1,1291.56,355.5Z" fill="#fff"/>
<path d="M1452.53,218.88q12.92,16.2,12.92,42.92v87.32a8.4,8.4,0,0,1-2.67,6.38,8.8,8.8,0,0,1-6.24,2.53,8.64,8.64,0,0,1-8.91-8.91V262.69q0-19.31-9.65-31.33t-29.85-12a53.28,53.28,0,0,0-42.77,21.83,36.24,36.24,0,0,0-7.13,21.53v86.43a8.91,8.91,0,1,1-17.82,0V216.06a8.91,8.91,0,1,1,17.82,0V232.4q8-12.77,23-21.24A61.84,61.84,0,0,1,1412,202.7Q1439.61,202.7,1452.53,218.88Z" fill="#fff"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="251" height="72" fill="none" viewBox="0 0 251 72">
<g clip-path="url(#a)">
<path fill="url(#b)"
d="M24.212 49.158C22.66 46.042 32.838 27.588 36 27.588c3.167.002 13.323 18.488 11.788 21.57-1.534 3.082-22.025 3.116-23.576 0" />
<path fill="url(#c)" fill-rule="evenodd"
d="M.482 64.995C-4.195 55.605 26.477 0 36 0c9.533 0 40.153 55.713 35.527 64.995s-66.368 9.39-71.045 0m12.254-8.148c3.064 6.152 43.518 6.084 46.548 0 3.03-6.086-17.032-42.586-23.275-42.586S9.671 50.694 12.736 56.847"
clip-rule="evenodd" />
<path fill="#fff"
d="M225.22 56c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.219-.218c-.054-.107-.054-.247-.054-.527V26.8c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.183c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527v2.895a7.9 7.9 0 0 1 3.419-3.254q2.261-1.103 5.074-1.103 3.308 0 5.845 1.434a10.1 10.1 0 0 1 4.026 4.026q1.434 2.536 1.434 5.9V55.2c0 .28 0 .42-.055.527a.5.5 0 0 1-.218.218c-.107.055-.247.055-.527.055h-5.625c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.218-.218c-.055-.107-.055-.247-.055-.527V38.408q0-2.978-1.709-4.688-1.654-1.764-4.357-1.764-2.702 0-4.412 1.764-1.654 1.766-1.654 4.688V55.2c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.218c-.107.055-.247.055-.527.055zm-11.54-33.363c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.218-.218c-.055-.107-.055-.247-.055-.527v-6.121c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .219.219c.054.107.054.247.054.527v6.12c0 .28 0 .42-.054.528a.5.5 0 0 1-.219.218c-.107.055-.247.055-.527.055zm0 33.363c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.218-.219c-.055-.107-.055-.247-.055-.527V26.8c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.218c.107-.055.247-.055.527-.055h5.624c.28 0 .42 0 .527.055a.5.5 0 0 1 .219.218c.054.107.054.247.054.527v28.4c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.219c-.107.054-.247.054-.527.054zm-16.712-.054c.107.054.247.054.527.054h5.625c.28 0 .42 0 .526-.054a.5.5 0 0 0 .219-.219c.055-.107.055-.247.055-.527V32.452h5.872c.28 0 .42 0 .527-.054a.5.5 0 0 0 .219-.219c.054-.107.054-.247.054-.527V26.8c0-.28 0-.42-.054-.527a.5.5 0 0 0-.219-.218c-.107-.055-.247-.055-.527-.055h-5.872v-.992q0-2.261 1.323-3.31 1.379-1.102 3.75-1.102.454 0 .939.044c.345.031.518.047.634-.004a.48.48 0 0 0 .241-.22c.061-.111.061-.274.061-.6V15.39c0-.304 0-.457-.061-.589a.7.7 0 0 0-.248-.284c-.122-.078-.261-.097-.537-.136a14.5 14.5 0 0 0-1.966-.126q-5.184 0-8.273 2.812t-3.088 7.942V26H186.53c-.3 0-.451 0-.58.05a.75.75 0 0 0-.296.205c-.091.104-.143.244-.248.526l-7.43 19.9-7.483-19.903c-.105-.28-.158-.42-.249-.524a.75.75 0 0 0-.296-.205c-.129-.049-.279-.049-.578-.049h-5.769c-.394 0-.591 0-.717.083a.5.5 0 0 0-.213.314c-.031.147.041.33.186.697L174.281 56l-.661 1.6q-.883 1.874-2.041 3.033-1.103 1.158-3.584 1.158-.883 0-1.875-.166a13 13 0 0 1-.73-.1c-.389-.066-.584-.099-.709-.053a.47.47 0 0 0-.26.22c-.066.116-.066.298-.066.663v4.329c0 .243 0 .365.045.481a.7.7 0 0 0 .189.266c.095.081.194.116.392.185q.684.24 1.47.351 1.158.22 2.371.22 4.246 0 7.059-2.426 2.867-2.37 4.577-6.728l10.517-26.58h5.72V55.2c0 .28 0 .42.055.527a.5.5 0 0 0 .218.219M154.363 56c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.219-.219c-.054-.107-.054-.247-.054-.527V15.054c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527V55.2c0 .28 0 .42-.055.527a.5.5 0 0 1-.218.219c-.107.054-.247.054-.527.054zm-11.621 0c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.219-.219c-.054-.107-.054-.247-.054-.527V15.054c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .219.219c.054.107.054.247.054.527V55.2c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.219c-.107.054-.247.054-.527.054zm-18.132.662q-4.632-.001-8.107-2.096a14.6 14.6 0 0 1-5.404-5.68q-1.93-3.585-1.93-7.942 0-4.522 1.93-7.996 1.985-3.53 5.349-5.57 3.42-2.04 7.61-2.04 4.688 0 7.942 2.04 3.253 1.986 4.963 5.294 1.71 3.309 1.709 7.335 0 .828-.11 1.654-.031.45-.12.841c-.037.165-.055.247-.115.33a.55.55 0 0 1-.208.168c-.095.04-.194.04-.393.04h-21.057q.33 3.309 2.537 5.294 2.205 1.986 5.459 1.985 2.482 0 4.191-1.047a8.2 8.2 0 0 0 2.206-1.986c.241-.316.362-.474.484-.542a.6.6 0 0 1 .352-.083c.139.006.296.083.608.236l4.269 2.094c.239.118.359.176.431.275a.52.52 0 0 1 .098.298c0 .122-.058.231-.172.45q-1.432 2.742-4.526 4.607-3.419 2.04-7.996 2.04m-.552-25.368q-2.702 0-4.687 1.654-1.93 1.6-2.537 4.577h14.118q-.22-2.757-2.151-4.466-1.875-1.765-4.743-1.765M90.801 56c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.218-.218C90 55.62 90 55.48 90 55.2v-5.294c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.218c.107-.055.247-.055.527-.055h1.572q2.646 0 4.19-1.489 1.6-1.545 1.6-4.08V15.715c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.219c.107-.054.247-.054.527-.054h5.956c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527v27.546q0 3.804-1.655 6.672-1.599 2.868-4.632 4.467-2.979 1.6-7.06 1.6z" />
</g>
<defs>
<linearGradient id="b" x1="12" x2="71.999" y1="30.001" y2="63.002"
gradientUnits="userSpaceOnUse">
<stop stop-color="#aa5cc3" />
<stop offset="1" stop-color="#00a4dc" />
</linearGradient>
<linearGradient id="c" x1="12" x2="71.999" y1="29.999" y2="63.001"
gradientUnits="userSpaceOnUse">
<stop stop-color="#aa5cc3" />
<stop offset="1" stop-color="#00a4dc" />
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M0 0h251v72H0z" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -4,12 +4,14 @@
} }
.topbar-wrapper .link:after { .topbar-wrapper .link:after {
content: url(../banner-dark.svg); content: '';
display: block; display: block;
-moz-box-sizing: border-box; background-image: url(../jellyfin.svg);
background-position: center;
background-repeat: no-repeat;
background-size: contain;
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; width: 220px;
max-height: 100%; height: 40px;
width: 150px;
} }
/* end logo */ /* end logo */

View File

@@ -103,11 +103,11 @@ namespace MediaBrowser.Common.Configuration
void MakeSanityCheckOrThrow(); void MakeSanityCheckOrThrow();
/// <summary> /// <summary>
/// Checks and creates the given path and adds it with a marker file if non existant. /// Checks and creates the given path and adds it with a marker file if non existent.
/// </summary> /// </summary>
/// <param name="path">The path to check.</param> /// <param name="path">The path to check.</param>
/// <param name="markerName">The common marker file name.</param> /// <param name="markerName">The common marker file name.</param>
/// <param name="recursive">Check for other settings paths recursivly.</param> /// <param name="recursive">Check for other settings paths recursively.</param>
void CreateAndCheckMarker(string path, string markerName, bool recursive = false); void CreateAndCheckMarker(string path, string markerName, bool recursive = false);
} }
} }

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.11.0</VersionPrefix> <VersionPrefix>10.11.6</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

@@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@@ -15,11 +13,12 @@ namespace MediaBrowser.Controller.Authentication
bool IsEnabled { get; } bool IsEnabled { get; }
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork); Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork);
Task<PinRedeemResult> RedeemPasswordResetPin(string pin); Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
} }
#nullable disable
public class PasswordPinCreationResult public class PasswordPinCreationResult
{ {
public string PinFile { get; set; } public string PinFile { get; set; }

View File

@@ -24,6 +24,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
@@ -1127,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities
var protocol = item.PathProtocol; var protocol = item.PathProtocol;
// Resolve the item path so everywhere we use the media source it will always point to
// the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
// path will return null, so it's safe to check for all paths.
var itemPath = item.Path;
if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo)
{
itemPath = linkInfo.FullName;
}
var info = new MediaSourceInfo var info = new MediaSourceInfo
{ {
Id = item.Id.ToString("N", CultureInfo.InvariantCulture), Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
@@ -1134,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities
MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
Name = GetMediaSourceName(item), Name = GetMediaSourceName(item),
Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
RunTimeTicks = item.RunTimeTicks, RunTimeTicks = item.RunTimeTicks,
Container = item.Container, Container = item.Container,
Size = item.Size, Size = item.Size,
@@ -1610,12 +1620,17 @@ namespace MediaBrowser.Controller.Entities
return isAllowed; return isAllowed;
} }
if (maxAllowedSubRating is not null) if (!maxAllowedRating.HasValue)
{ {
return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value; return true;
} }
return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value; if (ratingScore.Score != maxAllowedRating.Value)
{
return ratingScore.Score < maxAllowedRating.Value;
}
return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
} }
public ParentalRatingScore GetParentalRatingScore() public ParentalRatingScore GetParentalRatingScore()
@@ -2038,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities
public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
=> await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
/// <summary> /// <summary>
/// Validates that images within the item are still on the filesystem. /// Validates that images within the item are still on the filesystem.
/// </summary> /// </summary>

View File

@@ -715,14 +715,21 @@ namespace MediaBrowser.Controller.Entities
} }
else else
{ {
items = GetRecursiveChildren(user, query, out totalCount); // Save pagination params before clearing them to prevent pagination from happening
// before sorting. PostFilterAndSort will apply pagination after sorting.
var limit = query.Limit;
var startIndex = query.StartIndex;
query.Limit = null; query.Limit = null;
query.StartIndex = null; // override these here as they have already been applied query.StartIndex = null;
items = GetRecursiveChildren(user, query, out totalCount);
// Restore pagination params so PostFilterAndSort can apply them after sorting
query.Limit = limit;
query.StartIndex = startIndex;
} }
var result = PostFilterAndSort(items, query); return PostFilterAndSort(items, query);
result.TotalRecordCount = totalCount;
return result;
} }
if (this is not UserRootFolder if (this is not UserRootFolder
@@ -980,25 +987,19 @@ namespace MediaBrowser.Controller.Entities
else else
{ {
// need to pass this param to the children. // need to pass this param to the children.
// Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
var childQuery = new InternalItemsQuery var childQuery = new InternalItemsQuery
{ {
DisplayAlbumFolders = query.DisplayAlbumFolders, DisplayAlbumFolders = query.DisplayAlbumFolders,
Limit = query.Limit,
StartIndex = query.StartIndex,
NameStartsWith = query.NameStartsWith, NameStartsWith = query.NameStartsWith,
NameStartsWithOrGreater = query.NameStartsWithOrGreater, NameStartsWithOrGreater = query.NameStartsWithOrGreater,
NameLessThan = query.NameLessThan NameLessThan = query.NameLessThan
}; };
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter); items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
query.Limit = null;
query.StartIndex = null;
} }
var result = PostFilterAndSort(items, query); return PostFilterAndSort(items, query);
result.TotalRecordCount = totalItemCount;
return result;
} }
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query) protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
@@ -1034,7 +1035,15 @@ namespace MediaBrowser.Controller.Entities
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
} }
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager); var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
if (query.EnableTotalRecordCount)
{
result.TotalRecordCount = filteredItems.Count;
}
return result;
} }
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded( private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
@@ -1047,12 +1056,49 @@ namespace MediaBrowser.Controller.Entities
{ {
ArgumentNullException.ThrowIfNull(items); ArgumentNullException.ThrowIfNull(items);
if (CollapseBoxSetItems(query, queryParent, user, configurationManager)) if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
{ {
items = collectionManager.CollapseItemsWithinBoxSets(items, user); return items;
} }
return items; var config = configurationManager.Configuration;
bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
bool collapseSeries = config.EnableGroupingShowsIntoCollections;
if (user is null || (collapseMovies && collapseSeries))
{
return collectionManager.CollapseItemsWithinBoxSets(items, user);
}
if (!collapseMovies && !collapseSeries)
{
return items;
}
var collapsibleItems = new List<BaseItem>();
var remainingItems = new List<BaseItem>();
foreach (var item in items)
{
if ((collapseMovies && item is Movie) || (collapseSeries && item is Series))
{
collapsibleItems.Add(item);
}
else
{
remainingItems.Add(item);
}
}
if (collapsibleItems.Count == 0)
{
return remainingItems;
}
var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user);
return collapsedItems.Concat(remainingItems);
} }
private static bool CollapseBoxSetItems( private static bool CollapseBoxSetItems(
@@ -1083,24 +1129,26 @@ namespace MediaBrowser.Controller.Entities
} }
var param = query.CollapseBoxSetItems; var param = query.CollapseBoxSetItems;
if (param.HasValue)
if (!param.HasValue)
{ {
if (user is not null && query.IncludeItemTypes.Any(type => return param.Value && AllowBoxSetCollapsing(query);
(type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) ||
(type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections)))
{
return false;
}
if (query.IncludeItemTypes.Length == 0
|| query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
{
param = true;
}
} }
return param.HasValue && param.Value && AllowBoxSetCollapsing(query); var config = configurationManager.Configuration;
bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie);
bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Series);
bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
bool collapseSeries = config.EnableGroupingShowsIntoCollections;
if (user is not null)
{
bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries);
return canCollapse && AllowBoxSetCollapsing(query);
}
return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
} }
private static bool AllowBoxSetCollapsing(InternalItemsQuery request) private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
@@ -1358,13 +1406,6 @@ namespace MediaBrowser.Controller.Entities
.Where(e => query is null || UserViewBuilder.FilterItem(e, query)) .Where(e => query is null || UserViewBuilder.FilterItem(e, query))
.ToArray(); .ToArray();
if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
{
realChildren = realChildren
.OrderBy(e => e.ProductionYear ?? int.MaxValue)
.ToArray();
}
var childCount = realChildren.Length; var childCount = realChildren.Length;
if (result.Count < limit) if (result.Count < limit)
{ {

View File

@@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities
public string? Name { get; set; } public string? Name { get; set; }
public bool? UseRawName { get; set; }
public string? Person { get; set; } public string? Person { get; set; }
public Guid[] PersonIds { get; set; } public Guid[] PersonIds { get; set; }

View File

@@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities.Movies
if (sortBy == ItemSortBy.Default) if (sortBy == ItemSortBy.Default)
{ {
return items; return items;
} }
return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending); return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
@@ -136,6 +136,12 @@ namespace MediaBrowser.Controller.Entities.Movies
return Sort(children, user).ToArray(); return Sort(children, user).ToArray();
} }
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
{
var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query);
return Sort(children, user).ToArray();
}
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{ {
var children = base.GetRecursiveChildren(user, query, out totalCount); var children = base.GetRecursiveChildren(user, query, out totalCount);

View File

@@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null; query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey; query.SeriesPresentationUniqueKey = seriesKey;
query.IncludeItemTypes = new[] { BaseItemKind.Season }; query.IncludeItemTypes = new[] { BaseItemKind.Season };
query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) }; query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
if (user is not null && !user.DisplayMissingEpisodes) if (user is not null && !user.DisplayMissingEpisodes)
{ {
@@ -247,6 +247,10 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null; query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey; query.SeriesPresentationUniqueKey = seriesKey;
if (query.OrderBy.Count == 0)
{
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
}
if (query.IncludeItemTypes.Length == 0) if (query.IncludeItemTypes.Length == 0)
{ {

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@@ -61,4 +62,108 @@ public static class FileSystemHelper
} }
} }
} }
/// <summary>
/// Resolves a single link hop for the specified path.
/// </summary>
/// <remarks>
/// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT).
/// </remarks>
/// <param name="path">The file path to resolve.</param>
/// <returns>
/// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>.
/// </returns>
private static FileInfo? Resolve(string path)
{
try
{
return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo;
}
catch (IOException)
{
// Filesystem doesn't support links (e.g., exFAT).
return null;
}
}
/// <summary>
/// Gets the target of the specified file link.
/// </summary>
/// <remarks>
/// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
/// </remarks>
/// <param name="linkPath">The path of the file link.</param>
/// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
/// <returns>
/// A <see cref="FileInfo"/> if the <paramref name="linkPath"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
/// </returns>
public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
{
// Check if the file exists so the native resolve handler won't throw at us.
if (!File.Exists(linkPath))
{
return null;
}
if (!returnFinalTarget)
{
return Resolve(linkPath);
}
var targetInfo = Resolve(linkPath);
if (targetInfo is null || !targetInfo.Exists)
{
return targetInfo;
}
var currentPath = targetInfo.FullName;
var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
while (true)
{
var linkInfo = Resolve(currentPath);
if (linkInfo is null)
{
break;
}
var targetPath = linkInfo.FullName;
// If an infinite loop is detected, return the file info for the
// first link in the loop we encountered.
if (!visited.Add(targetPath))
{
return new FileInfo(targetPath);
}
targetInfo = linkInfo;
currentPath = targetPath;
// Exit if the target doesn't exist, so the native resolve handler won't throw at us.
if (!targetInfo.Exists)
{
break;
}
}
return targetInfo;
}
/// <summary>
/// Gets the target of the specified file link.
/// </summary>
/// <remarks>
/// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
/// </remarks>
/// <param name="fileInfo">The file info of the file link.</param>
/// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
/// <returns>
/// A <see cref="FileInfo"/> if the <paramref name="fileInfo"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
/// </returns>
public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false)
{
ArgumentNullException.ThrowIfNull(fileInfo);
return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget);
}
} }

View File

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

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@@ -29,7 +30,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
/// </summary> /// </summary>
private readonly Lock _taskLock = new(); private readonly Lock _taskLock = new();
private readonly BlockingCollection<TaskQueueItem> _tasks = new(); private readonly Channel<TaskQueueItem> _tasks = Channel.CreateUnbounded<TaskQueueItem>();
private volatile int _workCounter; private volatile int _workCounter;
private Task? _cleanupTask; private Task? _cleanupTask;
@@ -77,7 +78,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
lock (_taskLock) lock (_taskLock)
{ {
if (_tasks.Count > 0 || _workCounter > 0) if (_tasks.Reader.Count > 0 || _workCounter > 0)
{ {
_logger.LogDebug("Delay cleanup task, operations still running."); _logger.LogDebug("Delay cleanup task, operations still running.");
// tasks are still there so its still in use. Reschedule cleanup task. // tasks are still there so its still in use. Reschedule cleanup task.
@@ -144,9 +145,9 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
_deadlockDetector.Value = stopToken.TaskStop; _deadlockDetector.Value = stopToken.TaskStop;
try try
{ {
foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token)) while (!stopToken.GlobalStop.Token.IsCancellationRequested)
{ {
stopToken.GlobalStop.Token.ThrowIfCancellationRequested(); var item = await _tasks.Reader.ReadAsync(stopToken.GlobalStop.Token).ConfigureAwait(false);
try try
{ {
var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0; var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0;
@@ -242,7 +243,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
}; };
}).ToArray(); }).ToArray();
if (ShouldForceSequentialOperation()) if (ShouldForceSequentialOperation() || _deadlockDetector.Value is not null)
{ {
_logger.LogDebug("Process sequentially."); _logger.LogDebug("Process sequentially.");
try try
@@ -264,35 +265,14 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
for (var i = 0; i < workItems.Length; i++) for (var i = 0; i < workItems.Length; i++)
{ {
var item = workItems[i]!; var item = workItems[i]!;
_tasks.Add(item, CancellationToken.None); await _tasks.Writer.WriteAsync(item, CancellationToken.None).ConfigureAwait(false);
} }
if (_deadlockDetector.Value is not null) Worker();
{ _logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
_logger.LogDebug("Nested invocation detected, process in-place."); await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
try _logger.LogDebug("{NoWorkers} completed.", workItems.Length);
{ ScheduleTaskCleanup();
// we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved
while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token))
{
await ProcessItem(item).ConfigureAwait(false);
}
}
catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested)
{
// operation is cancelled. Do nothing.
}
_logger.LogDebug("process in-place done.");
}
else
{
Worker();
_logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
_logger.LogDebug("{NoWorkers} completed.", workItems.Length);
ScheduleTaskCleanup();
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -304,13 +284,12 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
} }
_disposed = true; _disposed = true;
_tasks.CompleteAdding(); _tasks.Writer.Complete();
foreach (var item in _taskRunners) foreach (var item in _taskRunners)
{ {
await item.Key.CancelAsync().ConfigureAwait(false); await item.Key.CancelAsync().ConfigureAwait(false);
} }
_tasks.Dispose();
if (_cleanupTask is not null) if (_cleanupTask is not null)
{ {
await _cleanupTask.ConfigureAwait(false); await _cleanupTask.ConfigureAwait(false);

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.11.0</VersionPrefix> <VersionPrefix>10.11.6</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

@@ -2378,6 +2378,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
// If SDR is the only supported range, we should not copy any of the HDR streams.
// All the following copy check assumes at least one HDR format is supported.
if (requestedRangeTypes.Length == 1 && requestHasSDR && videoStream.VideoRangeType != VideoRangeType.SDR)
{
return false;
}
// If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it. // If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it.
if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI) if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI)
{ {
@@ -2390,8 +2397,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR) || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus))) || (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
{ {
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG. // If the video stream is in HDR10+ or a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG) if (videoStream.VideoRangeType is VideoRangeType.HDR10Plus or VideoRangeType.HDR10 or VideoRangeType.HLG)
{ {
return false; return false;
} }
@@ -5942,28 +5949,37 @@ namespace MediaBrowser.Controller.MediaEncoding
var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap;
var swapOutputWandH = doRkVppTranspose && swapWAndH; var swapOutputWandH = doRkVppTranspose && swapWAndH;
var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts var outFormat = doOclTonemap ? "p010" : "nv12";
var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var doScaling = !string.IsNullOrEmpty(GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH));
if (!hasSubs if (!hasSubs
|| doRkVppTranspose || doRkVppTranspose
|| !isFullAfbcPipeline || !isFullAfbcPipeline
|| !string.IsNullOrEmpty(doScaling)) || doScaling)
{ {
var isScaleRatioSupported = IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f);
// RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation, // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation,
// but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it
if (!string.IsNullOrEmpty(doScaling) if (doScaling && !isScaleRatioSupported)
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
{ {
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format. // Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
// Use NV15 instead of P010 to avoid the issue. // Use NV15 instead of P010 to avoid the issue.
// SDR inputs are using BGRA formats already which is not affected. // SDR inputs are using BGRA formats already which is not affected.
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat; var intermediateFormat = doOclTonemap ? "nv15" : (isMjpegEncoder ? "bgra" : outFormat);
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1"; var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1";
mainFilters.Add(hwScaleFilterFirstPass); mainFilters.Add(hwScaleFilterFirstPass);
} }
// The RKMPP MJPEG encoder on some newer chip models no longer supports RGB input.
// Use 2pass here to enable RGA output of full-range YUV in the 2nd pass.
if (isMjpegEncoder && !doOclTonemap && ((doScaling && isScaleRatioSupported) || !doScaling))
{
var hwScaleFilterFirstPass = "vpp_rkrga=format=bgra:afbc=1";
mainFilters.Add(hwScaleFilterFirstPass);
}
if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
{ {
hwScaleFilter += $":transpose={transposeDir}"; hwScaleFilter += $":transpose={transposeDir}";
@@ -6343,6 +6359,21 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
} }
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
{
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
{
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
{
return null;
}
}
}
var decoder = hardwareAccelerationType switch var decoder = hardwareAccelerationType switch
{ {
HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth), HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth),
@@ -7023,8 +7054,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)) if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))
{ {
var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface); // there's an issue about AV1 AFBC on RK3588, disable it for now until it's fixed upstream
return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty); return GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
} }
} }

View File

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

View File

@@ -1,3 +1,4 @@
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using BDInfo.IO; using BDInfo.IO;
@@ -58,6 +59,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
} }
} }
private static bool IsHidden(ReadOnlySpan<char> name) => name.StartsWith('.');
/// <summary> /// <summary>
/// Gets the directories. /// Gets the directories.
/// </summary> /// </summary>
@@ -65,6 +68,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
public IDirectoryInfo[] GetDirectories() public IDirectoryInfo[] GetDirectories()
{ {
return _fileSystem.GetDirectories(_impl.FullName) return _fileSystem.GetDirectories(_impl.FullName)
.Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoDirectoryInfo(_fileSystem, x)) .Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
.ToArray(); .ToArray();
} }
@@ -76,6 +80,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
public IFileInfo[] GetFiles() public IFileInfo[] GetFiles()
{ {
return _fileSystem.GetFiles(_impl.FullName) return _fileSystem.GetFiles(_impl.FullName)
.Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoFileInfo(x)) .Select(x => new BdInfoFileInfo(x))
.ToArray(); .ToArray();
} }
@@ -88,6 +93,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
public IFileInfo[] GetFiles(string searchPattern) public IFileInfo[] GetFiles(string searchPattern)
{ {
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false) return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
.Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoFileInfo(x)) .Select(x => new BdInfoFileInfo(x))
.ToArray(); .ToArray();
} }
@@ -105,6 +111,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
new[] { searchPattern }, new[] { searchPattern },
false, false,
searchOption == SearchOption.AllDirectories) searchOption == SearchOption.AllDirectories)
.Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoFileInfo(x)) .Select(x => new BdInfoFileInfo(x))
.ToArray(); .ToArray();
} }

View File

@@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
if (!isAudio && _proberSupportsFirstVideoFrame) if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
{ {
args += " -show_frames -only_first_vframe"; args += " -show_frames -only_first_vframe";
} }
@@ -1122,7 +1122,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
private void StartProcess(ProcessWrapper process) private void StartProcess(ProcessWrapper process)
{ {
process.Process.Start(); process.Process.Start();
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
try
{
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName);
}
lock (_runningProcessesLock) lock (_runningProcessesLock)
{ {

View File

@@ -299,9 +299,12 @@ namespace MediaBrowser.MediaEncoding.Probing
// Handle WebM // Handle WebM
else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase))
{ {
// Limit WebM to supported codecs // Limit WebM to supported stream types and codecs.
if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)) // FFprobe can report "matroska,webm" for Matroska-like containers, so only keep "webm" if all streams are WebM-compatible.
|| (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)))) // Any stream that is not video nor audio is not supported in WebM and should disqualify the webm container probe result.
if (mediaStreams.Any(stream => stream.Type is not MediaStreamType.Video and not MediaStreamType.Audio)
|| mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
|| (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
{ {
splitFormat[i] = string.Empty; splitFormat[i] = string.Empty;
} }
@@ -853,7 +856,12 @@ namespace MediaBrowser.MediaEncoding.Probing
} }
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe // http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) if (string.IsNullOrEmpty(streamInfo.SampleAspectRatio)
&& string.IsNullOrEmpty(streamInfo.DisplayAspectRatio))
{
stream.IsAnamorphic = false;
}
else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
{ {
stream.IsAnamorphic = false; stream.IsAnamorphic = false;
} }
@@ -930,6 +938,15 @@ namespace MediaBrowser.MediaEncoding.Probing
{ {
stream.Rotation = data.Rotation; stream.Rotation = data.Rotation;
} }
// Parse video frame cropping metadata from side_data
// TODO: save them and make HW filters to apply them in HWA pipelines
else if (string.Equals(data.SideDataType, "Frame Cropping", StringComparison.OrdinalIgnoreCase))
{
// Streams containing artificially added frame cropping
// metadata should not be marked as anamorphic.
stream.IsAnamorphic = false;
}
} }
} }

View File

@@ -396,7 +396,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath); ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
// If subtitles get burned in fonts may need to be extracted from the media file // If subtitles get burned in fonts may need to be extracted from the media file
if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) if (state.SubtitleStream is not null && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode || state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding))
{ {
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
{ {

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.11.0</VersionPrefix> <VersionPrefix>10.11.6</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

@@ -1,11 +1,15 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
namespace MediaBrowser.Model.Users namespace MediaBrowser.Model.Users
{ {
public enum ForgotPasswordAction public enum ForgotPasswordAction
{ {
[Obsolete("Returning different actions represents a security concern.")]
ContactAdmin = 0, ContactAdmin = 0,
PinCode = 1, PinCode = 1,
[Obsolete("Returning different actions represents a security concern.")]
InNetworkRequired = 2 InNetworkRequired = 2
} }
} }

View File

@@ -88,7 +88,15 @@ namespace MediaBrowser.Providers.Manager
} }
} }
singular.AddRange(item.GetImages(ImageType.Backdrop)); foreach (var backdrop in item.GetImages(ImageType.Backdrop))
{
var imageInMetadataFolder = backdrop.Path.StartsWith(itemMetadataPath, StringComparison.OrdinalIgnoreCase);
if (imageInMetadataFolder || canDeleteLocal || item.IsSaveLocalMetadataEnabled())
{
singular.Add(backdrop);
}
}
PruneImages(item, singular); PruneImages(item, singular);
return singular.Count > 0; return singular.Count > 0;
@@ -466,10 +474,36 @@ namespace MediaBrowser.Providers.Manager
} }
} }
if (UpdateMultiImages(item, images, ImageType.Backdrop)) bool hasBackdrop = false;
bool backdropStoredWithMedia = false;
foreach (var image in images)
{ {
changed = true; if (image.Type != ImageType.Backdrop)
foundImageTypes.Add(ImageType.Backdrop); {
continue;
}
hasBackdrop = true;
if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase))
{
backdropStoredWithMedia = true;
break;
}
}
if (hasBackdrop)
{
if (UpdateMultiImages(item, images, ImageType.Backdrop))
{
changed = true;
}
if (backdropStoredWithMedia)
{
foundImageTypes.Add(ImageType.Backdrop);
}
} }
if (foundImageTypes.Count > 0) if (foundImageTypes.Count > 0)

View File

@@ -151,7 +151,10 @@ namespace MediaBrowser.Providers.Manager
.ConfigureAwait(false); .ConfigureAwait(false);
updateType |= beforeSaveResult; updateType |= beforeSaveResult;
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); if (isFirstRefresh)
{
await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false);
}
// Next run metadata providers // Next run metadata providers
if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None) if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
@@ -229,6 +232,11 @@ namespace MediaBrowser.Providers.Manager
if (file is not null) if (file is not null)
{ {
item.DateModified = file.LastWriteTimeUtc; item.DateModified = file.LastWriteTimeUtc;
if (!file.IsDirectory)
{
item.Size = file.Length;
}
} }
} }
@@ -239,7 +247,7 @@ namespace MediaBrowser.Providers.Manager
} }
// Save to database // Save to database
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false);
} }
return updateType; return updateType;
@@ -267,9 +275,14 @@ namespace MediaBrowser.Providers.Manager
} }
} }
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken) protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken)
{ {
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
if (reattachUserData)
{
await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
}
if (result.Item.SupportsPeople && result.People is not null) if (result.Item.SupportsPeople && result.People is not null)
{ {
var baseItem = result.Item; var baseItem = result.Item;
@@ -312,12 +325,8 @@ namespace MediaBrowser.Providers.Manager
{ {
if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType)) if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType))
{ {
if (isFullRefresh || updateType > ItemUpdateType.None) var children = GetChildrenForMetadataUpdates(item);
{ updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
var children = GetChildrenForMetadataUpdates(item);
updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
}
} }
var presentationUniqueKey = item.CreatePresentationUniqueKey(); var presentationUniqueKey = item.CreatePresentationUniqueKey();
@@ -339,7 +348,10 @@ namespace MediaBrowser.Providers.Manager
item.DateModified = info.LastWriteTimeUtc; item.DateModified = info.LastWriteTimeUtc;
if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded) if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
{ {
item.DateCreated = info.CreationTimeUtc; if (info.CreationTimeUtc > DateTime.MinValue)
{
item.DateCreated = info.CreationTimeUtc;
}
} }
if (item is Video video) if (item is Video video)
@@ -357,16 +369,24 @@ namespace MediaBrowser.Providers.Manager
protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType) protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType)
{ {
if (isFullRefresh || currentUpdateType > ItemUpdateType.None) if (item is Folder folder)
{ {
if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren) if (!isFullRefresh && currentUpdateType == ItemUpdateType.None)
{ {
return true; return folder.SupportsDateLastMediaAdded;
} }
if (item is Folder folder) if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
{ {
return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks; if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren)
{
return true;
}
if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks)
{
return true;
}
} }
} }
@@ -387,36 +407,42 @@ namespace MediaBrowser.Providers.Manager
{ {
var updateType = ItemUpdateType.None; var updateType = ItemUpdateType.None;
if (isFullRefresh || currentUpdateType > ItemUpdateType.None) if (item is Folder folder)
{ {
updateType |= UpdateCumulativeRunTimeTicks(item, children); if (folder.SupportsDateLastMediaAdded)
updateType |= UpdateDateLastMediaAdded(item, children);
// don't update user-changeable metadata for locked items
if (item.IsLocked)
{ {
return updateType; updateType |= UpdateDateLastMediaAdded(item, children);
} }
if (EnableUpdatingPremiereDateFromChildren) if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks)
{ {
updateType |= UpdatePremiereDate(item, children); updateType |= UpdateCumulativeRunTimeTicks(item, children);
} }
}
if (EnableUpdatingGenresFromChildren) if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked)
{ {
updateType |= UpdateGenres(item, children); return updateType;
} }
if (EnableUpdatingStudiosFromChildren) if (EnableUpdatingPremiereDateFromChildren)
{ {
updateType |= UpdateStudios(item, children); updateType |= UpdatePremiereDate(item, children);
} }
if (EnableUpdatingOfficialRatingFromChildren) if (EnableUpdatingGenresFromChildren)
{ {
updateType |= UpdateOfficialRating(item, children); updateType |= UpdateGenres(item, children);
} }
if (EnableUpdatingStudiosFromChildren)
{
updateType |= UpdateStudios(item, children);
}
if (EnableUpdatingOfficialRatingFromChildren)
{
updateType |= UpdateOfficialRating(item, children);
} }
return updateType; return updateType;

View File

@@ -721,8 +721,6 @@ namespace MediaBrowser.Providers.Manager
} }
} }
} }
_libraryManager.CreateItem(item, null);
} }
/// <summary> /// <summary>

View File

@@ -520,7 +520,7 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
Name = person.Name, Name = person.Name,
Type = person.Type, Type = person.Type,
Role = person.Role.Trim() Role = person.Role?.Trim()
}); });
} }
} }

View File

@@ -72,7 +72,7 @@ public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
} }
else else
{ {
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray();
} }
if (replaceData || targetItem.Shares.Count == 0) if (replaceData || targetItem.Shares.Count == 0)

View File

@@ -138,6 +138,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
} }
var item = itemResult.Item; var item = itemResult.Item;
item.IndexNumber = episodeNumber;
item.ParentIndexNumber = seasonNumber;
var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false); var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false);

View File

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

View File

@@ -177,8 +177,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var item = new Episode var item = new Episode
{ {
IndexNumber = info.IndexNumber, IndexNumber = episodeNumber,
ParentIndexNumber = info.ParentIndexNumber, ParentIndexNumber = seasonNumber,
IndexNumberEnd = info.IndexNumberEnd, IndexNumberEnd = info.IndexNumberEnd,
Name = episodeResult.Name, Name = episodeResult.Name,
PremiereDate = episodeResult.AirDate, PremiereDate = episodeResult.AirDate,
@@ -275,9 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember, CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember) PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
}) })
.Where(entry => .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
if (config.HideMissingCrewMembers) if (config.HideMissingCrewMembers)
{ {

View File

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

View File

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

View File

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

View File

@@ -68,12 +68,15 @@ namespace MediaBrowser.XbmcMetadata.Providers
{ {
var file = GetXmlFile(new ItemInfo(item), directoryService); var file = GetXmlFile(new ItemInfo(item), directoryService);
if (file is null) if (file?.Exists is not true)
{ {
return false; return false;
} }
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; var fileTime = _fileSystem.GetLastWriteTimeUtc(file);
// 1 minute tolerance to avoid detecting our own file writes
return (fileTime - item.DateLastSaved) > TimeSpan.FromMinutes(1);
} }
protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken); protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken);

View File

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

View File

@@ -58,7 +58,7 @@ for subproject in ${jellyfin_subprojects[@]}; do
done done
# Set the version in the GitHub issue template file # Set the version in the GitHub issue template file
sed -i "s|${old_version}|${new_version_sed}|g" ${issue_template_file} sed -i "s|${old_version}|${new_version_sed}|g" "${issue_template_file}"
# Stage the changed files for commit # Stage the changed files for commit
git add . git add .

View File

@@ -70,13 +70,14 @@ public static class JellyfinQueryHelperExtensions
bool invert = false) bool invert = false)
{ {
var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id); var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
var typeFilter = OneOrManyExpressionBuilder<ItemValue, ItemValueType>(itemValueTypes, iv => iv.Type);
return baseQuery.Where(item => return baseQuery.Where(item =>
context.ItemValues context.ItemValues
.Where(typeFilter)
.Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map }) .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map })
.Any(val => .Any(val =>
itemValueTypes.Contains(val.itemVal.Type) context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
&& context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
&& val.map.ItemId == item.Id) == EF.Constant(!invert)); && val.map.ItemId == item.Id) == EF.Constant(!invert));
} }

View File

@@ -52,10 +52,14 @@ public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
_logger = logger; _logger = logger;
_writePolicy = Policy _writePolicy = Policy
.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase)) .HandleInner<Exception>(e =>
e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
.WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle); .WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle);
_writeAsyncPolicy = Policy _writeAsyncPolicy = Policy
.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase)) .HandleInner<Exception>(e =>
e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
.WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle); .WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle);
void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context) void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context)

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