Compare commits

..

104 Commits

Author SHA1 Message Date
Cody Robibero
f6709a69e7 Explicitly load related items 2025-12-14 10:08:33 -07: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
217 changed files with 1468 additions and 2357 deletions

View File

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

View File

@@ -379,9 +379,6 @@ dotnet_diagnostic.CA1720.severity = suggestion
# disable warning CA1724: Type names should not match namespaces # disable warning CA1724: Type names should not match namespaces
dotnet_diagnostic.CA1724.severity = suggestion dotnet_diagnostic.CA1724.severity = suggestion
# disable warning CA1873: Avoid potentially expensive logging
dotnet_diagnostic.CA1873.severity = suggestion
# disable warning CA1805: Do not initialize unnecessarily # disable warning CA1805: Do not initialize unnecessarily
dotnet_diagnostic.CA1805.severity = suggestion dotnet_diagnostic.CA1805.severity = suggestion
@@ -403,10 +400,6 @@ dotnet_diagnostic.CA1861.severity = suggestion
# disable warning CA2000: Dispose objects before losing scope # disable warning CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = suggestion dotnet_diagnostic.CA2000.severity = suggestion
# TODO: Reevaluate when false positives are fixed: https://github.com/dotnet/roslyn-analyzers/issues/7699
# disable warning CA2025: Do not pass 'IDisposable' instances into unawaited tasks
dotnet_diagnostic.CA2025.severity = suggestion
# disable warning CA2253: Named placeholders should not be numeric values # disable warning CA2253: Named placeholders should not be numeric values
dotnet_diagnostic.CA2253.severity = suggestion dotnet_diagnostic.CA2253.severity = suggestion

15
.github/CODEOWNERS vendored
View File

@@ -1,11 +1,4 @@
# Joshua must review all changes to bump_version and any files it touches # Joshua must review all changes to deployment and build.sh
bump_version @joshuaboniface .ci/* @joshuaboniface
.github/ISSUE_TEMPLATE @joshuaboniface deployment/* @joshuaboniface
MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface build.sh @joshuaboniface
Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface
MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface
MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface
Emby.Naming/Emby.Naming.csproj @joshuaboniface
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface
# Core must approve all changes within the repo config
.github/ @jellyfin/core

View File

@@ -87,13 +87,7 @@ body:
label: Jellyfin Server version label: Jellyfin Server version
description: What version of Jellyfin are you using? description: What version of Jellyfin are you using?
options: options:
- 10.11.6 - 10.10.0+
- 10.11.5
- 10.11.4
- 10.11.3
- 10.11.2
- 10.11.1
- 10.11.0
- Master - Master
- Unstable - Unstable
- Older* - Older*
@@ -142,14 +136,13 @@ body:
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin] - **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode] - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.] - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **CPU Model**: [e.g. AMD Ryzen 5 9600X, Intel Core i7-8565U, etc.]
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.] - **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.] - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.] - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example] - **Base URL**: [e.g. none, yes: /example]
- **Networking**: [e.g. Host, Bridge/NAT] - **Networking**: [e.g. Host, Bridge/NAT]
- **Jellyfin Data Storage & Filesystem**: [e.g. local SATA SSD - ext4, local HDD - NTFS] - **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
- **Media Storage & Filesystem**: [e.g. Local HDD - ext4, SMB Share] - **Media Storage**: [e.g. Local HDD, SMB Share]
- **External Integrations**: [e.g. Jellystat, Jellyseerr] - **External Integrations**: [e.g. Jellystat, Jellyseerr]
value: | value: |
- OS: - OS:
@@ -160,14 +153,13 @@ body:
- FFmpeg Version: - FFmpeg Version:
- Playback Method: - Playback Method:
- Hardware Acceleration: - Hardware Acceleration:
- CPU Model:
- GPU Model: - GPU Model:
- Plugins: - Plugins:
- Reverse Proxy: - Reverse Proxy:
- Base URL: - Base URL:
- Networking: - Networking:
- Jellyfin Data Storage & Filesystem: - Jellyfin Data Storage:
- Media Storage & Filesystem: - Media Storage:
- External Integrations: - External Integrations:
render: markdown render: markdown
validations: validations:

View File

@@ -21,20 +21,17 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.0.x' dotnet-version: '9.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 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@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6

View File

@@ -17,16 +17,16 @@ jobs:
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@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.0.x' dotnet-version: '9.0.x'
- name: Build - name: Build
run: | run: |
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: abi-head name: abi-head
retention-days: 14 retention-days: 14
@@ -47,9 +47,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.0.x' dotnet-version: '9.0.x'
- name: Checkout common ancestor - name: Checkout common ancestor
env: env:
@@ -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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: abi-base name: abi-base
path: abi-base path: abi-base
@@ -106,7 +106,7 @@ jobs:
{ {
echo 'body<<EOF' echo 'body<<EOF'
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 || true )" COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
printf "\n${file}\n${COMPAT_OUTPUT}\n" printf "\n${file}\n${COMPAT_OUTPUT}\n"
fi fi

View File

@@ -20,21 +20,19 @@ jobs:
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@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: openapi-head name: openapi-head
retention-days: 14 retention-days: 14
if-no-files-found: error if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-base: openapi-base:
name: OpenAPI - BASE name: OpenAPI - BASE
@@ -48,7 +46,6 @@ jobs:
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: Checkout common ancestor - name: Checkout common ancestor
env: env:
HEAD_REF: ${{ github.head_ref }} HEAD_REF: ${{ github.head_ref }}
@@ -57,25 +54,23 @@ jobs:
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/* git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
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@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: openapi-base name: openapi-base
retention-days: 14 retention-days: 14
if-no-files-found: error if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-diff: openapi-diff:
permissions: permissions:
pull-requests: write pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
name: OpenAPI - Difference name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request_target' }} if: ${{ github.event_name == 'pull_request_target' }}
@@ -85,27 +80,71 @@ jobs:
- openapi-base - openapi-base
steps: steps:
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: openapi-base name: openapi-base
path: openapi-base path: openapi-base
- name: Workaround openapi-diff issue
- name: Detect OpenAPI changes run: |
id: openapi-diff sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0 sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
- name: Calculate OpenAPI difference
uses: docker://openapitools/openapi-diff
continue-on-error: true
with: with:
old-spec: openapi-base/openapi.json args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
new-spec: openapi-head/openapi.json - id: read-diff
markdown: openapi-changelog.md name: Read openapi-diff output
add-pr-comment: true run: |
github-token: ${{ secrets.GITHUB_TOKEN }} # Read and fix markdown
body=$(cat openapi-changes.md)
# Write to workflow summary
echo "$body" >> $GITHUB_STEP_SUMMARY
# Set ApiChanged var
if [ "$body" != '' ]; then
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
else
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
fi
# Add header/footer for diff comment
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
echo "<details>" >> openapi-changes-reply.md
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "$body" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "</details>" >> openapi-changes-reply.md
- name: Find difference comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body-path: openapi-changes-reply.md
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!--openapi-diff-workflow-comment-->
No changes to OpenAPI specification found. See history of this comment for previous changes.
publish-unstable: publish-unstable:
name: OpenAPI - Publish Unstable Spec name: OpenAPI - Publish Unstable Spec
@@ -119,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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
@@ -139,6 +178,7 @@ jobs:
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}" key: "${{ secrets.REPO_KEY }}"
debug: false debug: false
script_stop: false
script: | script: |
if ! test -d /run/workflows; then if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows sudo mkdir -p /run/workflows
@@ -180,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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
@@ -200,6 +240,7 @@ jobs:
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}" key: "${{ secrets.REPO_KEY }}"
debug: false debug: false
script_stop: false
script: | script: |
if ! test -d /run/workflows; then if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows sudo mkdir -p /run/workflows

View File

@@ -9,7 +9,7 @@ on:
pull_request: pull_request:
env: env:
SDK_VERSION: "10.0.x" SDK_VERSION: "9.0.x"
jobs: jobs:
run-tests: run-tests:
@@ -22,7 +22,7 @@ jobs:
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: ${{ env.SDK_VERSION }} dotnet-version: ${{ env.SDK_VERSION }}

View File

@@ -43,16 +43,13 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.14' python-version: '3.14'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r rename/requirements.txt run: pip install -r rename/requirements.txt
- name: run rename script - name: run rename script
run: python3 rename.py run: python3 rename.py
working-directory: ./rename working-directory: ./rename

View File

@@ -13,16 +13,13 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.14' python-version: '3.14'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r main-repo-triage/requirements.txt run: pip install -r main-repo-triage/requirements.txt
- name: check and comment issue - name: check and comment issue
working-directory: ./main-repo-triage working-directory: ./main-repo-triage
run: python3 single_issue_gha.py run: python3 single_issue_gha.py

View File

@@ -21,7 +21,6 @@ jobs:
with: with:
project: Current Release project: Current Release
action: delete action: delete
column: In progress
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project - name: Add to 'Release Next' project

6
.vscode/launch.json vendored
View File

@@ -6,7 +6,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient"], "args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
@@ -34,7 +34,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"], "args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",

View File

@@ -206,11 +206,7 @@
- [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)
- [Kirill Nikiforov](https://github.com/allmazz)
- [bjorntp](https://github.com/bjorntp)
- [martenumberto](https://github.com/martenumberto) - [martenumberto](https://github.com/martenumberto)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [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="8.0.0" /> <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" />
@@ -25,28 +25,33 @@
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<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="7.0.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" /> <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="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.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="10.0.2" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <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" />
@@ -58,10 +63,10 @@
<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.5" /> <PackageVersion Include="Polly" Version="8.6.5" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.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="10.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.1.1" /> <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" />
@@ -69,22 +74,26 @@
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" /> <PackageVersion Include="SharpFuzz" Version="2.2.0" />
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 --> <!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
<PackageVersion Include="SkiaSharp" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" /> <PackageVersion Include="Svg.Skia" Version="3.2.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.2" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />
<PackageVersion Include="System.Text.Json" Version="9.0.11" />
<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.10.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" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.61" /> <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

@@ -1,75 +0,0 @@
using System.Text.RegularExpressions;
namespace Emby.Naming.Book
{
/// <summary>
/// Helper class to retrieve basic metadata from a book filename.
/// </summary>
public static class BookFileNameParser
{
private const string NameMatchGroup = "name";
private const string IndexMatchGroup = "index";
private const string YearMatchGroup = "year";
private const string SeriesNameMatchGroup = "seriesName";
private static readonly Regex[] _nameMatches =
[
// seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required
new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"),
// last resort matches the whole string as the name
new Regex(@"(?<name>.*)")
];
/// <summary>
/// Parse a filename name to retrieve the book name, series name, index, and year.
/// </summary>
/// <param name="name">Book filename to parse for information.</param>
/// <returns>Returns <see cref="BookFileNameParserResult"/> object.</returns>
public static BookFileNameParserResult Parse(string? name)
{
var result = new BookFileNameParserResult();
if (name == null)
{
return result;
}
foreach (var regex in _nameMatches)
{
var match = regex.Match(name);
if (!match.Success)
{
continue;
}
if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success)
{
result.Name = nameGroup.Value.Trim();
}
if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index))
{
result.Index = index;
}
if (match.Groups.TryGetValue(YearMatchGroup, out Group? yearGroup) && yearGroup.Success && int.TryParse(yearGroup.Value, out var year))
{
result.Year = year;
}
if (match.Groups.TryGetValue(SeriesNameMatchGroup, out Group? seriesGroup) && seriesGroup.Success)
{
result.SeriesName = seriesGroup.Value.Trim();
}
break;
}
return result;
}
}
}

View File

@@ -1,41 +0,0 @@
using System;
namespace Emby.Naming.Book
{
/// <summary>
/// Data object used to pass metadata parsed from a book filename.
/// </summary>
public class BookFileNameParserResult
{
/// <summary>
/// Initializes a new instance of the <see cref="BookFileNameParserResult"/> class.
/// </summary>
public BookFileNameParserResult()
{
Name = null;
Index = null;
Year = null;
SeriesName = null;
}
/// <summary>
/// Gets or sets the name of the book.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the book index.
/// </summary>
public int? Index { get; set; }
/// <summary>
/// Gets or sets the publication year.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets the series name.
/// </summary>
public string? SeriesName { get; set; }
}
}

View File

@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.12.0</VersionPrefix> <VersionPrefix>10.11.4</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

@@ -17,13 +17,6 @@ namespace Emby.Naming.TV
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")] [GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
private static partial Regex SeriesNameRegex(); private static partial Regex SeriesNameRegex();
/// <summary>
/// Regex that matches titles with year in parentheses. Captures the title (which may be
/// numeric) before the year, i.e. turns "1923 (2022)" into "1923".
/// </summary>
[GeneratedRegex(@"(?<title>.+?)\s*\(\d{4}\)")]
private static partial Regex TitleWithYearRegex();
/// <summary> /// <summary>
/// Resolve information about series from path. /// Resolve information about series from path.
/// </summary> /// </summary>
@@ -34,20 +27,6 @@ namespace Emby.Naming.TV
{ {
string seriesName = Path.GetFileName(path); string seriesName = Path.GetFileName(path);
// First check if the filename matches a title with year pattern (handles numeric titles)
if (!string.IsNullOrEmpty(seriesName))
{
var titleWithYearMatch = TitleWithYearRegex().Match(seriesName);
if (titleWithYearMatch.Success)
{
seriesName = titleWithYearMatch.Groups["title"].Value.Trim();
return new SeriesInfo(path)
{
Name = seriesName
};
}
}
SeriesPathParserResult result = SeriesPathParser.Parse(options, path); SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
if (result.Success) if (result.Success)
{ {

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -137,27 +136,19 @@ namespace Emby.Naming.Video
if (videos.Count > 1) if (videos.Count > 1)
{ {
var groups = videos var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
.Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
.Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
.GroupBy(x => x.resolutionMatch.Success)
.ToList();
videos.Clear(); videos.Clear();
StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
foreach (var group in groups) foreach (var group in groups)
{ {
if (group.Key) if (group.Key)
{ {
videos.InsertRange(0, group videos.InsertRange(0, group
.OrderByDescending(x => x.resolutionMatch.Value, comparer) .OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator())
.ThenBy(x => x.filename, comparer) .ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
.Select(x => x.value));
} }
else else
{ {
videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value)); videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
} }
} }
} }

View File

@@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -39,24 +39,22 @@ namespace Emby.Server.Implementations.Cryptography
{ {
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal)) if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
{ {
var iterations = GetIterationsParameter(hash);
return hash.Hash.SequenceEqual( return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2( Rfc2898DeriveBytes.Pbkdf2(
password, password,
hash.Salt, hash.Salt,
iterations, int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA1, HashAlgorithmName.SHA1,
32)); 32));
} }
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal)) if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
{ {
var iterations = GetIterationsParameter(hash);
return hash.Hash.SequenceEqual( return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2( Rfc2898DeriveBytes.Pbkdf2(
password, password,
hash.Salt, hash.Salt,
iterations, int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA512, HashAlgorithmName.SHA512,
DefaultOutputLength)); DefaultOutputLength));
} }
@@ -64,27 +62,6 @@ namespace Emby.Server.Implementations.Cryptography
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}"); throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
} }
/// <summary>
/// Extracts and validates the iterations parameter from a password hash.
/// </summary>
/// <param name="hash">The password hash containing parameters.</param>
/// <returns>The number of iterations.</returns>
/// <exception cref="FormatException">Thrown when iterations parameter is missing or invalid.</exception>
private static int GetIterationsParameter(PasswordHash hash)
{
if (!hash.Parameters.TryGetValue("iterations", out var iterationsStr))
{
throw new FormatException($"Password hash with id '{hash.Id}' is missing required 'iterations' parameter.");
}
if (!int.TryParse(iterationsStr, CultureInfo.InvariantCulture, out var iterations))
{
throw new FormatException($"Password hash with id '{hash.Id}' has invalid 'iterations' parameter: '{iterationsStr}'.");
}
return iterations;
}
/// <inheritdoc /> /// <inheritdoc />
public byte[] GenerateSalt() public byte[] GenerateSalt()
=> GenerateSalt(DefaultSaltLength); => GenerateSalt(DefaultSaltLength);

View File

@@ -27,6 +27,7 @@
<PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="prometheus-net.DotNetRuntime" /> <PackageReference Include="prometheus-net.DotNetRuntime" />
@@ -38,7 +39,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -352,12 +352,6 @@ 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

@@ -98,11 +98,5 @@ 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

@@ -83,7 +83,6 @@ 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

@@ -2144,7 +2144,7 @@ namespace Emby.Server.Implementations.Library
item.ValidateImages(); item.ValidateImages();
await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false); _itemRepository.SaveImages(item);
RegisterItem(item); RegisterItem(item);
} }
@@ -2202,12 +2202,6 @@ 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)
@@ -3201,7 +3195,19 @@ 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);
CreateShortcut(virtualFolderPath, pathInfo); 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);
if (saveLibraryOptions) if (saveLibraryOptions)
{ {
@@ -3366,24 +3372,5 @@ 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

@@ -5,12 +5,12 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Book;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers.Books namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
@@ -35,24 +35,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var extension = Path.GetExtension(args.Path.AsSpan()); var extension = Path.GetExtension(args.Path.AsSpan());
if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{ {
return null; // It's a book
}
var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path));
return new Book return new Book
{ {
Path = args.Path, Path = args.Path,
Name = result.Name ?? string.Empty, IsInMixedFolder = true
IndexNumber = result.Index,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
IsInMixedFolder = true,
}; };
} }
return null;
}
private Book GetBook(ItemResolveArgs args) private Book GetBook(ItemResolveArgs args)
{ {
var bookFiles = args.FileSystemChildren.Where(f => var bookFiles = args.FileSystemChildren.Where(f =>
@@ -64,22 +59,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
StringComparison.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
}).ToList(); }).ToList();
// directory is only considered a book when it contains exactly one supported file // Don't return a Book if there is more (or less) than one document in the directory
// other library structures with multiple books to a directory will get picked up as individual files
if (bookFiles.Count != 1) if (bookFiles.Count != 1)
{ {
return null; return null;
} }
var result = BookFileNameParser.Parse(Path.GetFileName(args.Path));
return new Book return new Book
{ {
Path = bookFiles[0].FullName, Path = bookFiles[0].FullName
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? string.Empty,
}; };
} }
} }

View File

@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value); results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
} }
if (query.Limit.HasValue && query.Limit.Value > 0) if (query.Limit.HasValue)
{ {
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count)); results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
} }

View File

@@ -2,13 +2,13 @@
"Albums": "ألبومات", "Albums": "ألبومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}", "AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق", "Application": "تطبيق",
"Artists": "فنانون", "Artists": "الفنانون",
"AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}", "AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
"Books": "الكتب", "Books": "الكتب",
"CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}", "CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
"Channels": "القنوات", "Channels": "القنوات",
"ChapterNameValue": "الفصل {0}", "ChapterNameValue": "الفصل {0}",
"Collections": "مجموعات", "Collections": "المجموعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل", "DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}", "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
@@ -16,7 +16,7 @@
"Folders": "المجلدات", "Folders": "المجلدات",
"Genres": "التصنيفات", "Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم", "HeaderAlbumArtists": "فناني الألبوم",
"HeaderContinueWatching": "متابعة المشاهدة", "HeaderContinueWatching": "إستئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة", "HeaderFavoriteEpisodes": "الحلقات المفضلة",

View File

@@ -16,7 +16,7 @@
"Collections": "Калекцыі", "Collections": "Калекцыі",
"Default": "Па змаўчанні", "Default": "Па змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі", "Folders": "Тэчкі",
"Favorites": "Абранае", "Favorites": "Абранае",
"External": "Знешні", "External": "Знешні",
"Genres": "Жанры", "Genres": "Жанры",
@@ -95,7 +95,7 @@
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску", "ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
"Shows": "Шоу", "Shows": "Шоу",
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.", "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
"SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}", "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
"TvShows": "Тэлепраграма", "TvShows": "Тэлепраграма",
"Undefined": "Нявызначана", "Undefined": "Нявызначана",
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны", "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
@@ -104,7 +104,7 @@
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}", "UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}", "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку", "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
"ValueSpecialEpisodeName": "Спецвыпуск - {0}", "ValueSpecialEpisodeName": "Спецэпізод - {0}",
"VersionNumber": "Версія {0}", "VersionNumber": "Версія {0}",
"TasksMaintenanceCategory": "Абслугоўванне", "TasksMaintenanceCategory": "Абслугоўванне",
"TasksLibraryCategory": "Бібліятэка", "TasksLibraryCategory": "Бібліятэка",
@@ -114,7 +114,7 @@
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.", "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў", "TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
"TaskRefreshLibrary": "Сканаваць бібліятэку", "TaskRefreshLibrary": "Сканаваць бібліятэку",
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метаданыя.", "TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
"TaskCleanLogs": "Ачысціць журнал", "TaskCleanLogs": "Ачысціць журнал",
"TaskRefreshPeople": "Абнавіць выканаўцаў", "TaskRefreshPeople": "Абнавіць выканаўцаў",
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.", "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
@@ -137,5 +137,5 @@
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
} }

View File

@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}", "CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
"Books": "Llyfrau", "Books": "Llyfrau",
"AuthenticationSucceededWithUserName": "{0} wedii ddilysun llwyddiannus", "AuthenticationSucceededWithUserName": "{0} wedii ddilysun llwyddiannus",
"Artists": "Crewyr", "Artists": "Artistiaid",
"AppDeviceValues": "Ap: {0}, Dyfais: {1}", "AppDeviceValues": "Ap: {0}, Dyfais: {1}",
"Albums": "Albwmau", "Albums": "Albwmau",
"Genres": "Genres", "Genres": "Genres",
@@ -67,7 +67,7 @@
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain", "NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru", "MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru", "MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}", "FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}",
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau", "ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}", "UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}", "UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
@@ -123,14 +123,5 @@
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod", "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.", "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
"TaskCleanCache": "Gwaghau Ffolder Cache", "TaskCleanCache": "Gwaghau Ffolder Cache",
"HearingImpaired": "Nam ar y clyw", "HearingImpaired": "Nam ar y clyw"
"TaskAudioNormalization": "Gwastatau Sain",
"TaskAudioNormalizationDescription": "Yn sganio ffeiliau am ddata gwastatau sain.",
"TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay",
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
"TaskCleanCollectionsAndPlaylists": "Glanhau casgliadau a rhestrau chwarae",
"TaskCleanCollectionsAndPlaylistsDescription": "Dileu eitemau o gasgliadau a rhestrau chwarae sydd ddim yn bodoli bellach.",
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
} }

View File

@@ -9,9 +9,9 @@
"Channels": "Kanäle", "Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}", "ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen", "Collections": "Sammlungen",
"DeviceOfflineWithName": "{0} ist offline", "DeviceOfflineWithName": "{0} hat die Verbindung getrennt",
"DeviceOnlineWithName": "{0} ist online", "DeviceOnlineWithName": "{0} ist verbunden",
"FailedLoginAttemptWithUserName": "Anmeldung von {0} fehlgeschlagen", "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
"Favorites": "Favoriten", "Favorites": "Favoriten",
"Folders": "Verzeichnisse", "Folders": "Verzeichnisse",
"Genres": "Genres", "Genres": "Genres",
@@ -21,7 +21,7 @@
"HeaderFavoriteArtists": "Lieblingsinterpreten", "HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden", "HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteShows": "Lieblingsserien", "HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingssongs", "HeaderFavoriteSongs": "Lieblingslieder",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "Live TV",
"HeaderNextUp": "Als Nächstes", "HeaderNextUp": "Als Nächstes",
"HeaderRecordingGroups": "Aufnahme-Gruppen", "HeaderRecordingGroups": "Aufnahme-Gruppen",
@@ -46,7 +46,7 @@
"NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.", "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar", "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert", "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
"NotificationOptionAudioPlayback": "Audio wird abgespielt", "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
"NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt", "NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt",
"NotificationOptionCameraImageUploaded": "Foto hochgeladen", "NotificationOptionCameraImageUploaded": "Foto hochgeladen",
"NotificationOptionInstallationFailed": "Installation fehlgeschlagen", "NotificationOptionInstallationFailed": "Installation fehlgeschlagen",
@@ -57,8 +57,8 @@
"NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert", "NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert",
"NotificationOptionServerRestartRequired": "Serverneustart notwendig", "NotificationOptionServerRestartRequired": "Serverneustart notwendig",
"NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen", "NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen",
"NotificationOptionUserLockedOut": "Benutzer gesperrt", "NotificationOptionUserLockedOut": "Benutzer ausgeschlossen",
"NotificationOptionVideoPlayback": "Video wird abgespielt", "NotificationOptionVideoPlayback": "Videowiedergabe gestartet",
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt", "NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
"Photos": "Fotos", "Photos": "Fotos",
"Playlists": "Wiedergabelisten", "Playlists": "Wiedergabelisten",
@@ -82,7 +82,7 @@
"UserCreatedWithName": "Benutzer {0} wurde erstellt", "UserCreatedWithName": "Benutzer {0} wurde erstellt",
"UserDeletedWithName": "Benutzer {0} wurde gelöscht", "UserDeletedWithName": "Benutzer {0} wurde gelöscht",
"UserDownloadingItemWithValues": "{0} lädt {1} herunter", "UserDownloadingItemWithValues": "{0} lädt {1} herunter",
"UserLockedOutWithName": "Benutzer {0} wurde gesperrt", "UserLockedOutWithName": "Benutzer {0} wurde ausgeschlossen",
"UserOfflineFromDevice": "{0} wurde getrennt von {1}", "UserOfflineFromDevice": "{0} wurde getrennt von {1}",
"UserOnlineFromDevice": "{0} ist online von {1}", "UserOnlineFromDevice": "{0} ist online von {1}",
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert", "UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
@@ -97,25 +97,25 @@
"TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.", "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
"TaskRefreshChannels": "Kanäle aktualisieren", "TaskRefreshChannels": "Kanäle aktualisieren",
"TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.", "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
"TaskCleanTranscode": "Transkodierungsverzeichnis leeren", "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
"TaskUpdatePlugins": "Plugins aktualisieren", "TaskUpdatePlugins": "Plugins aktualisieren",
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
"TaskRefreshPeople": "Personen aktualisieren", "TaskRefreshPeople": "Personen aktualisieren",
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.", "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
"TaskCleanLogs": "Protokollverzeichnis leeren", "TaskCleanLogs": "Log-Verzeichnis aufräumen",
"TaskRefreshLibraryDescription": "Durchsucht deine Medienbibliothek nach neuen Dateien und aktualisiert Metadaten.", "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.",
"TaskRefreshLibrary": "Medien-Bibliothek scannen", "TaskRefreshLibrary": "Medien-Bibliothek scannen",
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videokapitel.", "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
"TaskRefreshChapterImages": "Kapitelvorschauen erstellen", "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren",
"TaskCleanCacheDescription": "Löscht Cache-Dateien, die vom System nicht mehr benötigt werden.", "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.",
"TaskCleanCache": "Cache-Verzeichnis leeren", "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen",
"TasksChannelsCategory": "Internet-Kanäle", "TasksChannelsCategory": "Internet-Kanäle",
"TasksApplicationCategory": "Anwendung", "TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek", "TasksLibraryCategory": "Bibliothek",
"TasksMaintenanceCategory": "Wartung", "TasksMaintenanceCategory": "Wartung",
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
"TaskCleanActivityLog": "Aktivitätsverlauf bereinigen", "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen",
"Undefined": "Undefiniert", "Undefined": "Undefiniert",
"Forced": "Erzwungen", "Forced": "Erzwungen",
"Default": "Standard", "Default": "Standard",

View File

@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado", "ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series", "Shows": "Programas",
"Songs": "Canciones", "Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",

View File

@@ -30,7 +30,7 @@
"ItemAddedWithName": "{0} fue agregado a la biblioteca", "ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca", "ItemRemovedWithName": "{0} fue removido de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}", "LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo corriendo: {0}", "LabelRunningTimeValue": "Tiempo de reproducción: {0}",
"Latest": "Recientes", "Latest": "Recientes",
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado", "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}", "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",

View File

@@ -72,7 +72,7 @@
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval", "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
"NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.", "NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
"NameSeasonUnknown": "Tundmatu hooaeg", "NameSeasonUnknown": "Tundmatu hooaeg",
"NameSeasonNumber": "{0}. hooaeg", "NameSeasonNumber": "Hooaeg {0}",
"NameInstallFailed": "{0} paigaldamine nurjus", "NameInstallFailed": "{0} paigaldamine nurjus",
"MusicVideos": "Muusikavideod", "MusicVideos": "Muusikavideod",
"Music": "Muusika", "Music": "Muusika",
@@ -137,5 +137,5 @@
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.", "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht", "TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Puhasta kasutajaandmed", "CleanupUserDataTask": "Puhasta kasutajaandmed",
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud." "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mis pole enam vähemalt 90 päeva saadaval olnud."
} }

View File

@@ -11,7 +11,7 @@
"Collections": "Sammlungen", "Collections": "Sammlungen",
"DeviceOfflineWithName": "{0} wurde getrennt", "DeviceOfflineWithName": "{0} wurde getrennt",
"DeviceOnlineWithName": "{0} ist verbunden", "DeviceOnlineWithName": "{0} ist verbunden",
"FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}", "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
"Favorites": "Favorite", "Favorites": "Favorite",
"Folders": "Ordner", "Folders": "Ordner",
"Genres": "Genre", "Genres": "Genre",

View File

@@ -129,12 +129,5 @@
"TaskAudioNormalization": "श्रव्य सामान्यीकरण", "TaskAudioNormalization": "श्रव्य सामान्यीकरण",
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें", "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ", "TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है", "TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
} }

View File

@@ -136,7 +136,5 @@
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션", "TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.", "TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
"TaskDownloadMissingLyrics": "누락된 가사 다운로드", "TaskDownloadMissingLyrics": "누락된 가사 다운로드",
"TaskDownloadMissingLyricsDescription": "가사 다운로드", "TaskDownloadMissingLyricsDescription": "가사 다운로드"
"CleanupUserDataTask": "사용자 데이터 정리 작업",
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
} }

View File

@@ -1,9 +0,0 @@
{
"Albums": "Pukaemi",
"AppDeviceValues": "Taupānga: {0}, Pūrere: {1}",
"Application": "Taupānga",
"Artists": "Kaiwaiata",
"AuthenticationSucceededWithUserName": "{0} has been successfully authenticated",
"Books": "Ngā pukapuka",
"CameraImageUploadedFrom": "Kua tuku ake he whakaahua kāmera hou mai i {0}"
}

View File

@@ -2,12 +2,12 @@
"AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}", "AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}",
"Application": "അപ്ലിക്കേഷൻ", "Application": "അപ്ലിക്കേഷൻ",
"AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു", "AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു",
"CameraImageUploadedFrom": "{0} എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്‌ലോഡുചെയ്‌തു", "CameraImageUploadedFrom": "Camera 0 from എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്‌ലോഡുചെയ്‌തു",
"ChapterNameValue": "അധ്യായം {0}", "ChapterNameValue": "അധ്യായം {0}",
"DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു", "DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു",
"DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു", "DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു",
"FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു", "FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
"Forced": "നിർബന്ധിതമായി", "Forced": "നിർബന്ധിച്ചു",
"HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ", "HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ",
"HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ", "HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ",
"HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ", "HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ",
@@ -114,7 +114,7 @@
"Artists": "കലാകാരന്മാർ", "Artists": "കലാകാരന്മാർ",
"Shows": "ഷോകൾ", "Shows": "ഷോകൾ",
"Default": "സ്ഥിരസ്ഥിതി", "Default": "സ്ഥിരസ്ഥിതി",
"Favorites": "പ്രിയപ്പെട്ടവ", "Favorites": "പ്രിയങ്കരങ്ങൾ",
"Books": "പുസ്തകങ്ങൾ", "Books": "പുസ്തകങ്ങൾ",
"Genres": "വിഭാഗങ്ങൾ", "Genres": "വിഭാഗങ്ങൾ",
"Channels": "ചാനലുകൾ", "Channels": "ചാനലുകൾ",

View File

@@ -3,7 +3,7 @@
"HeaderNextUp": "Дараа нь", "HeaderNextUp": "Дараа нь",
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх", "HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
"Songs": "Дуунууд", "Songs": "Дуунууд",
"Playlists": "Тоглуулах жагсаалтууд", "Playlists": "Playlist-ууд",
"Movies": "Кинонууд", "Movies": "Кинонууд",
"Latest": "Сүүлийн үеийн", "Latest": "Сүүлийн үеийн",
"Genres": "Төрлүүд", "Genres": "Төрлүүд",
@@ -71,7 +71,7 @@
"Forced": "Хүчээр", "Forced": "Хүчээр",
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид", "HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
"HeaderFavoriteAlbums": "Дуртай цомгууд", "HeaderFavoriteAlbums": "Дуртай цомгууд",
"HeaderLiveTV": "Шууд ТВ", "HeaderLiveTV": "Шууд",
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд", "HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
"HearingImpaired": "Сонсголын бэрхшээлтэй", "HearingImpaired": "Сонсголын бэрхшээлтэй",
"HomeVideos": "Үндсэн дүрсүүд", "HomeVideos": "Үндсэн дүрсүүд",
@@ -109,7 +109,7 @@
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв", "ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу", "ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
"Shows": "Шоу", "Shows": "Шоу",
"Sync": "Синхрончлох", "Sync": "Дахин",
"System": "Систем", "System": "Систем",
"TvShows": "ТВ нэвтрүүлгүүд", "TvShows": "ТВ нэвтрүүлгүүд",
"Undefined": "Танисангүй", "Undefined": "Танисангүй",

View File

@@ -132,10 +132,5 @@
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा", "TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण", "TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.", "TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो", "TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
} }

View File

@@ -126,7 +126,5 @@
"TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်", "TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်",
"TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း", "TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း",
"TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်", "TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်",
"HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ", "HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ"
"TaskDownloadMissingLyrics": "ကျန်နေသောသီချင်းစာသားများအား ဒေါင်းလုတ်ဆွဲပါ",
"TaskDownloadMissingLyricsDescription": "သီချင်းများအတွက် သီချင်းစာသား ဒေါင်းလုတ်ဆွဲပါ"
} }

View File

@@ -134,8 +134,6 @@
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।", "TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ", "TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ", "TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।", "TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।", "TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
} }

View File

@@ -125,8 +125,8 @@
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.", "TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
"HearingImpaired": "Niedosłyszący", "HearingImpaired": "Niedosłyszący",
"TaskRefreshTrickplayImages": "Generuj obrazy Trickplay", "TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy Trickplay dla filmów we włączonych bibliotekach.", "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.",
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.", "TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania", "TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
"TaskAudioNormalization": "Normalizacja dźwięku", "TaskAudioNormalization": "Normalizacja dźwięku",

View File

@@ -16,7 +16,7 @@
"Collections": "Barrels", "Collections": "Barrels",
"ItemAddedWithName": "{0} is now with yer treasure", "ItemAddedWithName": "{0} is now with yer treasure",
"Default": "Normal-like", "Default": "Normal-like",
"FailedLoginAttemptWithUserName": "Ye failed to enter from {0}", "FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
"Favorites": "Finest Loot", "Favorites": "Finest Loot",
"ItemRemovedWithName": "{0} was taken from yer treasure", "ItemRemovedWithName": "{0} was taken from yer treasure",
"LabelIpAddressValue": "Ship's coordinates: {0}", "LabelIpAddressValue": "Ship's coordinates: {0}",
@@ -113,10 +113,5 @@
"TaskCleanCache": "Sweep the Cache Chest", "TaskCleanCache": "Sweep the Cache Chest",
"TaskRefreshChapterImages": "Claim chapter portraits", "TaskRefreshChapterImages": "Claim chapter portraits",
"TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.", "TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.",
"TaskRefreshLibrary": "Scan the Treasure Trove", "TaskRefreshLibrary": "Scan the Treasure Trove"
"TasksChannelsCategory": "Channels o' thy Internet",
"TaskRefreshTrickplayImages": "Summon the picture tricks",
"TaskRefreshTrickplayImagesDescription": "Summons picture trick previews for videos in ye enabled book roost",
"TaskUpdatePlugins": "Resummon yer Plugins",
"TaskCleanTranscode": "Swab Ye Transcode Directory"
} }

View File

@@ -5,7 +5,7 @@
"Artists": "Artistas", "Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Books": "Livros", "Books": "Livros",
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}", "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
"Channels": "Canais", "Channels": "Canais",
"ChapterNameValue": "Capítulo {0}", "ChapterNameValue": "Capítulo {0}",
"Collections": "Coleções", "Collections": "Coleções",
@@ -125,8 +125,8 @@
"TaskKeyframeExtractor": "Extrator de Quadros-chave", "TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo", "External": "Externo",
"HearingImpaired": "Surdo", "HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay", "TaskRefreshTrickplayImages": "Gerar imagens de truques",
"TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.", "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -135,7 +135,5 @@
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย", "TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย",
"TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี", "TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี",
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment", "TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay", "TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay"
"CleanupUserDataTask": "ส่วนงานล้างข้อมูลผู้ใช้",
"CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน"
} }

View File

@@ -39,7 +39,7 @@
"TasksMaintenanceCategory": "Bảo Trì", "TasksMaintenanceCategory": "Bảo Trì",
"VersionNumber": "Phiên Bản {0}", "VersionNumber": "Phiên Bản {0}",
"ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn", "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
"UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {1} trên {2}", "UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}", "UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
"UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}", "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}", "UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",

View File

@@ -23,7 +23,7 @@
"HeaderFavoriteShows": "最愛的節目", "HeaderFavoriteShows": "最愛的節目",
"HeaderFavoriteSongs": "最愛的歌曲", "HeaderFavoriteSongs": "最愛的歌曲",
"HeaderLiveTV": "電視直播", "HeaderLiveTV": "電視直播",
"HeaderNextUp": "繼續觀看", "HeaderNextUp": "接著播放",
"HeaderRecordingGroups": "錄製組", "HeaderRecordingGroups": "錄製組",
"HomeVideos": "家庭影片", "HomeVideos": "家庭影片",
"Inherit": "繼承", "Inherit": "繼承",
@@ -127,8 +127,8 @@
"HearingImpaired": "聽力障礙", "HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像", "TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
"TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。", "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
"TaskExtractMediaSegments": "掃描媒體分段資訊", "TaskExtractMediaSegments": "掃描媒體段落",
"TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件獲取媒體段。", "TaskExtractMediaSegmentsDescription": "從MediaSegment中被允許的插件獲取媒體段。",
"TaskDownloadMissingLyrics": "下載欠缺歌詞", "TaskDownloadMissingLyrics": "下載欠缺歌詞",
"TaskDownloadMissingLyricsDescription": "下載歌詞", "TaskDownloadMissingLyricsDescription": "下載歌詞",
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單", "TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
@@ -137,6 +137,5 @@
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。", "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
"CleanupUserDataTask": "用戶資料清理工作", "CleanupUserDataTask": "用戶資料清理工作"
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
} }

View File

@@ -38,7 +38,6 @@ 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!;
@@ -162,7 +161,6 @@ 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);
} }
@@ -170,32 +168,21 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc /> /// <inheritdoc />
public CultureDto? FindLanguageInfo(string language) public CultureDto? FindLanguageInfo(string language)
{
if (string.IsNullOrEmpty(language))
{
return null;
}
return _cultureCache.GetOrAdd(
language,
static (lang, cultures) =>
{ {
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
for (var i = 0; i < cultures.Count; i++) for (var i = 0; i < _cultures.Count; i++)
{ {
var culture = cultures[i]; var culture = _cultures[i];
if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|| lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|| culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase) || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
|| lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
{ {
return culture; return culture;
} }
} }
return null; return default;
},
_cultures);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -324,19 +311,15 @@ 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
var ratingsDictionary = GetParentalRatingsDictionary(); // If it has no ratings, use the US ratings
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, starting with US // If we don't find anything, check all ratings systems
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

@@ -1,11 +1,11 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Globalization;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting namespace Emby.Server.Implementations.Sorting
{ {
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Sorting
ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y); ArgumentNullException.ThrowIfNull(y);
return CultureInfo.InvariantCulture.CompareInfo.Compare(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault(), CompareOptions.NumericOrdering); return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());
} }
} }
} }

View File

@@ -266,7 +266,7 @@ namespace Emby.Server.Implementations.TV
items = items.Skip(query.StartIndex.Value); items = items.Skip(query.StartIndex.Value);
} }
if (query.Limit.HasValue && query.Limit.Value > 0) if (query.Limit.HasValue)
{ {
items = items.Take(query.Limit.Value); items = items.Take(query.Limit.Value);
} }

View File

@@ -156,11 +156,6 @@ 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);
@@ -562,7 +557,7 @@ namespace Emby.Server.Implementations.Updates
} }
stream.Position = 0; stream.Position = 0;
await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken); ZipFile.ExtractToDirectory(stream, targetDir, true);
// Ensure we create one or populate existing ones with missing data. // Ensure we create one or populate existing ones with missing data.
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);

View File

@@ -1,16 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums; using Jellyfin.Api.Constants;
using Jellyfin.Data.Queries; using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers; namespace Jellyfin.Api.Controllers;
@@ -35,20 +32,10 @@ public class ActivityLogController : BaseJellyfinApiController
/// <summary> /// <summary>
/// Gets activity log entries. /// Gets activity log entries.
/// </summary> /// </summary>
/// <param name="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="minDate">The minimum date.</param> /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
/// <param name="maxDate">The maximum date.</param> /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
/// <param name="hasUserId">Filter log entries if it has user id, or not.</param>
/// <param name="name">Filter by name.</param>
/// <param name="overview">Filter by overview.</param>
/// <param name="shortOverview">Filter by short overview.</param>
/// <param name="type">Filter by type.</param>
/// <param name="itemId">Filter by item id.</param>
/// <param name="username">Filter by username.</param>
/// <param name="severity">Filter by log severity.</param>
/// <param name="sortBy">Specify one or more sort orders. Format: SortBy=Name,Type.</param>
/// <param name="sortOrder">Sort Order..</param>
/// <response code="200">Activity log returned.</response> /// <response code="200">Activity log returned.</response>
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
[HttpGet("Entries")] [HttpGet("Entries")]
@@ -57,62 +44,14 @@ public class ActivityLogController : BaseJellyfinApiController
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] DateTime? minDate, [FromQuery] DateTime? minDate,
[FromQuery] DateTime? maxDate, [FromQuery] bool? hasUserId)
[FromQuery] bool? hasUserId,
[FromQuery] string? name,
[FromQuery] string? overview,
[FromQuery] string? shortOverview,
[FromQuery] string? type,
[FromQuery] Guid? itemId,
[FromQuery] string? username,
[FromQuery] LogLevel? severity,
[FromQuery] ActivityLogSortBy[]? sortBy,
[FromQuery] SortOrder[]? sortOrder)
{ {
var query = new ActivityLogQuery return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
{ {
Skip = startIndex, Skip = startIndex,
Limit = limit, Limit = limit,
MinDate = minDate, MinDate = minDate,
MaxDate = maxDate, HasUserId = hasUserId
HasUserId = hasUserId, }).ConfigureAwait(false);
Name = name,
Overview = overview,
ShortOverview = shortOverview,
Type = type,
ItemId = itemId,
Username = username,
Severity = severity,
OrderBy = GetOrderBy(sortBy ?? [], sortOrder ?? []),
};
return await _activityManager.GetPagedResultAsync(query).ConfigureAwait(false);
}
private static (ActivityLogSortBy SortBy, SortOrder SortOrder)[] GetOrderBy(
IReadOnlyList<ActivityLogSortBy> sortBy,
IReadOnlyList<SortOrder> requestedSortOrder)
{
if (sortBy.Count == 0)
{
return [];
}
var result = new (ActivityLogSortBy, SortOrder)[sortBy.Count];
var i = 0;
for (; i < requestedSortOrder.Count; i++)
{
result[i] = (sortBy[i], requestedSortOrder[i]);
}
// Add remaining elements with the first specified SortOrder
// or the default one if no SortOrders are specified
var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
for (; i < sortBy.Count; i++)
{
result[i] = (sortBy[i], order);
}
return result;
} }
} }

View File

@@ -122,6 +122,7 @@ public class ArtistsController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null; User? user = null;
@@ -325,6 +326,7 @@ public class ArtistsController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null; User? user = null;
@@ -465,7 +467,7 @@ public class ArtistsController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var item = _libraryManager.GetArtist(name, dtoOptions); var item = _libraryManager.GetArtist(name, dtoOptions);

View File

@@ -50,6 +50,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -106,6 +107,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -157,6 +159,7 @@ public class AudioController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate,
@@ -214,6 +217,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -270,6 +274,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -321,6 +326,7 @@ public class AudioController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate,

View File

@@ -65,7 +65,7 @@ public class CollectionController : BaseJellyfinApiController
UserIds = new[] { userId } UserIds = new[] { userId }
}).ConfigureAwait(false); }).ConfigureAwait(false);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var dto = _dtoService.GetBaseItemDto(item, dtoOptions); var dto = _dtoService.GetBaseItemDto(item, dtoOptions);

View File

@@ -122,6 +122,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -181,6 +182,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -236,6 +238,7 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate,
@@ -361,6 +364,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -421,6 +425,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -476,6 +481,7 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate,
@@ -537,6 +543,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
@@ -594,6 +601,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate, [FromQuery] int? maxStreamingBitrate,
@@ -646,6 +654,7 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate ?? maxStreamingBitrate, AudioBitRate = audioBitRate ?? maxStreamingBitrate,
@@ -704,6 +713,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -761,6 +771,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -815,6 +826,7 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate,
@@ -875,6 +887,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
@@ -930,6 +943,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate, [FromQuery] int? maxStreamingBitrate,
@@ -982,6 +996,7 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate ?? maxStreamingBitrate, AudioBitRate = audioBitRate ?? maxStreamingBitrate,
@@ -1045,6 +1060,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -1108,6 +1124,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -1164,6 +1181,7 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate,
@@ -1229,6 +1247,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
@@ -1290,6 +1309,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate, [FromQuery] int? maxStreamingBitrate,
@@ -1344,6 +1364,7 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate ?? maxStreamingBitrate, AudioBitRate = audioBitRate ?? maxStreamingBitrate,
@@ -1565,6 +1586,16 @@ public class DynamicHlsController : BaseJellyfinApiController
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
if (state.BaseRequest.BreakOnNonKeyFrames)
{
// FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
// breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
// to produce a missing part of video stream before first keyframe is encountered, which may lead to
// awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
_logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
state.BaseRequest.BreakOnNonKeyFrames = false;
}
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
@@ -1715,6 +1746,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
return copyArgs + " -copypriorss:a:0 0";
}
return copyArgs; return copyArgs;
} }
@@ -1803,9 +1839,8 @@ public class DynamicHlsController : BaseJellyfinApiController
{ {
if (isActualOutputVideoCodecHevc) if (isActualOutputVideoCodecHevc)
{ {
// 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. // Prefer dvh1 to dvhe
var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1"; args += " -tag:v:0 dvh1 -strict -2";
args += $" -tag:v:0 {codecTag} -strict -2";
} }
else if (isActualOutputVideoCodecAv1) else if (isActualOutputVideoCodecAv1)
{ {

View File

@@ -94,6 +94,7 @@ public class GenresController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = userId.IsNullOrEmpty() User? user = userId.IsNullOrEmpty()
@@ -158,7 +159,8 @@ public class GenresController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions()
.AddClientFields(User);
Genre? item; Genre? item;
if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))

View File

@@ -90,6 +90,7 @@ public class InstantMixController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions); return GetResult(items, user, limit, dtoOptions);
@@ -133,6 +134,7 @@ public class InstantMixController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions); return GetResult(items, user, limit, dtoOptions);
@@ -176,6 +178,7 @@ public class InstantMixController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions); return GetResult(items, user, limit, dtoOptions);
@@ -211,6 +214,7 @@ public class InstantMixController : BaseJellyfinApiController
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions); return GetResult(items, user, limit, dtoOptions);
@@ -254,6 +258,7 @@ public class InstantMixController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions); return GetResult(items, user, limit, dtoOptions);
@@ -297,6 +302,7 @@ public class InstantMixController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions); return GetResult(items, user, limit, dtoOptions);
@@ -379,6 +385,7 @@ public class InstantMixController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions); return GetResult(items, user, limit, dtoOptions);

View File

@@ -180,14 +180,11 @@ public class ItemUpdateController : BaseJellyfinApiController
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
info.ContentType = configuredContentType; info.ContentType = configuredContentType;
if (inheritedContentType is null if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows)
|| inheritedContentType == CollectionType.tvshows
|| inheritedContentType == CollectionType.movies)
{ {
info.ContentTypeOptions = info.ContentTypeOptions info.ContentTypeOptions = info.ContentTypeOptions
.Where(i => string.IsNullOrWhiteSpace(i.Value) .Where(i => string.IsNullOrWhiteSpace(i.Value)
|| string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase) || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase))
|| string.Equals(i.Value, "Movies", StringComparison.OrdinalIgnoreCase))
.ToArray(); .ToArray();
} }
} }
@@ -421,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.Trim()); hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
} }
} }
@@ -429,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.Trim()); hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
} }
} }

View File

@@ -268,6 +268,7 @@ public class ItemsController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (includeItemTypes.Length == 1 if (includeItemTypes.Length == 1
@@ -848,6 +849,7 @@ public class ItemsController : BaseJellyfinApiController
var parentIdGuid = parentId ?? Guid.Empty; var parentIdGuid = parentId ?? Guid.Empty;
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var ancestorIds = Array.Empty<Guid>(); var ancestorIds = Array.Empty<Guid>();

View File

@@ -188,7 +188,7 @@ public class LibraryController : BaseJellyfinApiController
item = parent; item = parent;
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var items = themeItems var items = themeItems
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
.ToArray(); .ToArray();
@@ -261,7 +261,7 @@ public class LibraryController : BaseJellyfinApiController
item = parent; item = parent;
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var items = themeItems var items = themeItems
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
.ToArray(); .ToArray();
@@ -497,7 +497,7 @@ public class LibraryController : BaseJellyfinApiController
var baseItemDtos = new List<BaseItemDto>(); var baseItemDtos = new List<BaseItemDto>();
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
BaseItem? parent = item.GetParent(); BaseItem? parent = item.GetParent();
while (parent is not null) while (parent is not null)
@@ -557,7 +557,7 @@ public class LibraryController : BaseJellyfinApiController
items = items.Where(i => i.IsHidden == val).ToList(); items = items.Where(i => i.IsHidden == val).ToList();
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
return new QueryResult<BaseItemDto>(resultArray); return new QueryResult<BaseItemDto>(resultArray);
} }
@@ -759,7 +759,8 @@ public class LibraryController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>(); return new QueryResult<BaseItemDto>();
} }
var dtoOptions = new DtoOptions { Fields = fields }; var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User);
var program = item as IHasProgramAttributes; var program = item as IHasProgramAttributes;
bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer; bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;

View File

@@ -342,17 +342,6 @@ 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

@@ -170,6 +170,7 @@ public class LiveTvController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var channelResult = _liveTvManager.GetInternalChannels( var channelResult = _liveTvManager.GetInternalChannels(
@@ -241,7 +242,8 @@ public class LiveTvController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions()
.AddClientFields(User);
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
@@ -295,6 +297,7 @@ public class LiveTvController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
return await _liveTvManager.GetRecordingsAsync( return await _liveTvManager.GetRecordingsAsync(
@@ -441,7 +444,8 @@ public class LiveTvController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions()
.AddClientFields(User);
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
@@ -631,6 +635,7 @@ public class LiveTvController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
} }
@@ -685,6 +690,7 @@ public class LiveTvController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] } var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] }
.AddClientFields(User)
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []); .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []);
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
} }
@@ -754,6 +760,7 @@ public class LiveTvController : BaseJellyfinApiController
}; };
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
} }

View File

@@ -74,7 +74,8 @@ public class MoviesController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty() var user = userId.IsNullOrEmpty()
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }; var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User);
var categories = new List<RecommendationDto>(); var categories = new List<RecommendationDto>();

View File

@@ -94,6 +94,7 @@ public class MusicGenresController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = userId.IsNullOrEmpty() User? user = userId.IsNullOrEmpty()
@@ -147,7 +148,7 @@ public class MusicGenresController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
MusicGenre? item; MusicGenre? item;

View File

@@ -81,6 +81,7 @@ public class PersonsController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = userId.IsNullOrEmpty() User? user = userId.IsNullOrEmpty()
@@ -120,7 +121,8 @@ public class PersonsController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions()
.AddClientFields(User);
var item = _libraryManager.GetPerson(name); var item = _libraryManager.GetPerson(name);
if (item is null) if (item is null)

View File

@@ -548,6 +548,7 @@ public class PlaylistsController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);

View File

@@ -89,6 +89,7 @@ public class StudiosController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = userId.IsNullOrEmpty() User? user = userId.IsNullOrEmpty()
@@ -141,7 +142,7 @@ public class StudiosController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var item = _libraryManager.GetStudio(name); var item = _libraryManager.GetStudio(name);
if (!userId.IsNullOrEmpty()) if (!userId.IsNullOrEmpty())

View File

@@ -77,7 +77,7 @@ public class SuggestionsController : BaseJellyfinApiController
user = _userManager.GetUserById(requestUserId); user = _userManager.GetUserById(requestUserId);
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{ {
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },

View File

@@ -99,6 +99,7 @@ public class TvShowsController : BaseJellyfinApiController
} }
var options = new DtoOptions { Fields = fields } var options = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var result = _tvSeriesManager.GetNextUp( var result = _tvSeriesManager.GetNextUp(
@@ -160,6 +161,7 @@ public class TvShowsController : BaseJellyfinApiController
var parentIdGuid = parentId ?? Guid.Empty; var parentIdGuid = parentId ?? Guid.Empty;
var options = new DtoOptions { Fields = fields } var options = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
@@ -229,6 +231,7 @@ public class TvShowsController : BaseJellyfinApiController
List<BaseItem> episodes; List<BaseItem> episodes;
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey(); var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
@@ -357,6 +360,7 @@ public class TvShowsController : BaseJellyfinApiController
}); });
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);

View File

@@ -83,6 +83,7 @@ public class UniversalAudioController : BaseJellyfinApiController
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param> /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param> /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
/// <response code="200">Audio stream returned.</response> /// <response code="200">Audio stream returned.</response>
/// <response code="302">Redirected to remote audio stream.</response> /// <response code="302">Redirected to remote audio stream.</response>
@@ -113,6 +114,7 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia, [FromQuery] bool? enableRemoteMedia,
[FromQuery] bool enableAudioVbrEncoding = true, [FromQuery] bool enableAudioVbrEncoding = true,
[FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true) [FromQuery] bool enableRedirection = true)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
@@ -125,7 +127,7 @@ public class UniversalAudioController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
@@ -206,6 +208,7 @@ public class UniversalAudioController : BaseJellyfinApiController
EnableAutoStreamCopy = true, EnableAutoStreamCopy = true,
AllowAudioStreamCopy = true, AllowAudioStreamCopy = true,
AllowVideoStreamCopy = true, AllowVideoStreamCopy = true,
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate, AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
MaxAudioBitDepth = maxAudioBitDepth, MaxAudioBitDepth = maxAudioBitDepth,
@@ -239,6 +242,7 @@ public class UniversalAudioController : BaseJellyfinApiController
EnableAutoStreamCopy = true, EnableAutoStreamCopy = true,
AllowAudioStreamCopy = true, AllowAudioStreamCopy = true,
AllowVideoStreamCopy = true, AllowVideoStreamCopy = true,
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate, AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate), AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
@@ -259,6 +263,7 @@ public class UniversalAudioController : BaseJellyfinApiController
string? transcodingContainer, string? transcodingContainer,
string? audioCodec, string? audioCodec,
MediaStreamProtocol? transcodingProtocol, MediaStreamProtocol? transcodingProtocol,
bool? breakOnNonKeyFrames,
int? transcodingAudioChannels, int? transcodingAudioChannels,
int? maxAudioSampleRate, int? maxAudioSampleRate,
int? maxAudioBitDepth, int? maxAudioBitDepth,
@@ -293,6 +298,7 @@ public class UniversalAudioController : BaseJellyfinApiController
Container = transcodingContainer ?? "mp3", Container = transcodingContainer ?? "mp3",
AudioCodec = audioCodec ?? "mp3", AudioCodec = audioCodec ?? "mp3",
Protocol = transcodingProtocol ?? MediaStreamProtocol.http, Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
} }
}; };

View File

@@ -13,7 +13,6 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@@ -95,7 +94,7 @@ public class UserLibraryController : BaseJellyfinApiController
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
@@ -134,7 +133,7 @@ public class UserLibraryController : BaseJellyfinApiController
} }
var item = _libraryManager.GetUserRootFolder(); var item = _libraryManager.GetUserRootFolder();
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
@@ -181,7 +180,7 @@ public class UserLibraryController : BaseJellyfinApiController
} }
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
return new QueryResult<BaseItemDto>(dtos); return new QueryResult<BaseItemDto>(dtos);
@@ -423,7 +422,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
if (item is IHasTrailers hasTrailers) if (item is IHasTrailers hasTrailers)
{ {
var trailers = hasTrailers.LocalTrailers; var trailers = hasTrailers.LocalTrailers;
@@ -479,7 +478,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
return Ok(item return Ok(item
.GetExtras() .GetExtras()
@@ -550,6 +549,7 @@ public class UserLibraryController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var list = _userViewManager.GetLatestItems( var list = _userViewManager.GetLatestItems(
@@ -569,7 +569,7 @@ public class UserLibraryController : BaseJellyfinApiController
var item = i.Item2[0]; var item = i.Item2[0];
var childCount = 0; var childCount = 0;
if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum || i.Item1 is Series )) if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
{ {
item = i.Item1; item = i.Item1;
childCount = i.Item2.Count; childCount = i.Item2.Count;

View File

@@ -86,7 +86,7 @@ public class UserViewsController : BaseJellyfinApiController
var folders = _userViewManager.GetUserViews(query); var folders = _userViewManager.GetUserViews(query);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions().AddClientFields(User);
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId]; dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user)); var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));

View File

@@ -111,6 +111,7 @@ public class VideosController : BaseJellyfinApiController
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions();
dtoOptions = dtoOptions.AddClientFields(User);
BaseItemDto[] items; BaseItemDto[] items;
if (item is Video video) if (item is Video video)
@@ -270,6 +271,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -328,6 +330,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -384,6 +387,7 @@ public class VideosController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true, EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate,
@@ -508,6 +512,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -566,6 +571,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
@@ -619,6 +625,7 @@ public class VideosController : BaseJellyfinApiController
enableAutoStreamCopy, enableAutoStreamCopy,
allowVideoStreamCopy, allowVideoStreamCopy,
allowAudioStreamCopy, allowAudioStreamCopy,
breakOnNonKeyFrames,
audioSampleRate, audioSampleRate,
maxAudioBitDepth, maxAudioBitDepth,
audioBitRate, audioBitRate,

View File

@@ -89,6 +89,7 @@ public class YearsController : BaseJellyfinApiController
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = userId.IsNullOrEmpty() User? user = userId.IsNullOrEmpty()
@@ -181,7 +182,8 @@ public class YearsController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions()
.AddClientFields(User);
if (!userId.IsNullOrEmpty()) if (!userId.IsNullOrEmpty())
{ {

View File

@@ -1,6 +1,10 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace Jellyfin.Api.Extensions; namespace Jellyfin.Api.Extensions;
@@ -9,6 +13,55 @@ namespace Jellyfin.Api.Extensions;
/// </summary> /// </summary>
public static class DtoExtensions public static class DtoExtensions
{ {
/// <summary>
/// Add additional fields depending on client.
/// </summary>
/// <remarks>
/// Use in place of GetDtoOptions.
/// Legacy order: 2.
/// </remarks>
/// <param name="dtoOptions">DtoOptions object.</param>
/// <param name="user">Current claims principal.</param>
/// <returns>Modified DtoOptions object.</returns>
internal static DtoOptions AddClientFields(
this DtoOptions dtoOptions, ClaimsPrincipal user)
{
string? client = user.GetClient();
// No client in claim
if (string.IsNullOrEmpty(client))
{
return dtoOptions;
}
if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
{
if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
client.Contains("classic", StringComparison.OrdinalIgnoreCase))
{
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.RecursiveItemCount];
}
}
if (!dtoOptions.ContainsField(ItemFields.ChildCount))
{
if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
client.Contains("classic", StringComparison.OrdinalIgnoreCase) ||
client.Contains("roku", StringComparison.OrdinalIgnoreCase) ||
client.Contains("samsung", StringComparison.OrdinalIgnoreCase) ||
client.Contains("androidtv", StringComparison.OrdinalIgnoreCase))
{
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.ChildCount];
}
}
return dtoOptions;
}
/// <summary> /// <summary>
/// Add additional DtoOptions. /// Add additional DtoOptions.
/// </summary> /// </summary>

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(queryString); var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
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,21 +173,10 @@ public class DynamicHlsHelper
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
} }
// 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))
{
queryString += "&AllowVideoStreamCopy=false";
}
// Main stream // Main stream
var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
var playlistUrl = baseUrl + queryString;
var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString); playlistUrl += queryString;
var subtitleStreams = state.MediaSource var subtitleStreams = state.MediaSource
.MediaStreams .MediaStreams
@@ -209,36 +198,37 @@ 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 AV1 and HEVC SDR entrances for backward compatibility. // Provide SDR HEVC entrance for backward compatibility.
foreach (var sdrVideoCodec in new[] { "av1", "hevc" }) if (encodingOptions.AllowHevcEncoding
{ && !encodingOptions.AllowAv1Encoding
var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
&& 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) && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRange == VideoRange.HDR) && state.VideoStream.VideoRange == VideoRange.HDR
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
// Force AV1 and HEVC Main Profile and disable video stream copy. var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
state.OutputVideoCodec = sdrVideoCodec; if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
{
var sdrPlaylistQuery = playlistQuery; // Force HEVC Main Profile and disable video stream copy.
sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec; state.OutputVideoCodec = "hevc";
sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main"; var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false"; sdrVideoUrl += "&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);
@@ -248,30 +238,12 @@ 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 (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) if (encodingOptions.AllowHevcEncoding
&& !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
@@ -301,15 +273,12 @@ public class DynamicHlsHelper
var variation = GetBitrateVariation(totalBitrate); var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation; var newBitrate = totalBitrate - variation;
var variantQuery = playlistQuery; var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
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;
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture); variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
} }
@@ -754,9 +723,7 @@ public class DynamicHlsHelper
{ {
if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
{ {
string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
? state.AudioStream?.Profile : state.GetRequestedProfiles("aac").FirstOrDefault();
return HlsCodecStringHelpers.GetAACString(profile); return HlsCodecStringHelpers.GetAACString(profile);
} }
@@ -790,19 +757,6 @@ public class DynamicHlsHelper
return HlsCodecStringHelpers.GetOPUSString(); return HlsCodecStringHelpers.GetOPUSString();
} }
if (string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
{
return HlsCodecStringHelpers.GetTRUEHDString();
}
if (string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
{
// lavc only support encoding DTS core profile
string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) ? state.AudioStream?.Profile : "DTS";
return HlsCodecStringHelpers.GetDTSString(profile);
}
return string.Empty; return string.Empty;
} }
@@ -909,6 +863,23 @@ 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

@@ -41,11 +41,6 @@ public static class HlsCodecStringHelpers
/// </summary> /// </summary>
public const string OPUS = "Opus"; public const string OPUS = "Opus";
/// <summary>
/// Codec name for TRUEHD.
/// </summary>
public const string TRUEHD = "mlpa";
/// <summary> /// <summary>
/// Gets a MP3 codec string. /// Gets a MP3 codec string.
/// </summary> /// </summary>
@@ -64,7 +59,7 @@ public static class HlsCodecStringHelpers
{ {
StringBuilder result = new StringBuilder("mp4a", 9); StringBuilder result = new StringBuilder("mp4a", 9);
if (string.Equals(profile, "HE-AAC", StringComparison.OrdinalIgnoreCase)) if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
{ {
result.Append(".40.5"); result.Append(".40.5");
} }
@@ -122,46 +117,6 @@ public static class HlsCodecStringHelpers
return OPUS; return OPUS;
} }
/// <summary>
/// Gets an TRUEHD codec string.
/// </summary>
/// <returns>TRUEHD codec string.</returns>
public static string GetTRUEHDString()
{
return TRUEHD;
}
/// <summary>
/// Gets an DTS codec string.
/// </summary>
/// <param name="profile">DTS profile.</param>
/// <returns>DTS codec string.</returns>
public static string GetDTSString(string? profile)
{
if (string.Equals(profile, "DTS", StringComparison.OrdinalIgnoreCase)
|| string.Equals(profile, "DTS-ES", StringComparison.OrdinalIgnoreCase)
|| string.Equals(profile, "DTS 96/24", StringComparison.OrdinalIgnoreCase))
{
return "dtsc";
}
if (string.Equals(profile, "DTS-HD HRA", StringComparison.OrdinalIgnoreCase)
|| string.Equals(profile, "DTS-HD MA", StringComparison.OrdinalIgnoreCase)
|| string.Equals(profile, "DTS-HD MA + DTS:X", StringComparison.OrdinalIgnoreCase)
|| string.Equals(profile, "DTS-HD MA + DTS:X IMAX", StringComparison.OrdinalIgnoreCase))
{
return "dtsh";
}
if (string.Equals(profile, "DTS Express", StringComparison.OrdinalIgnoreCase))
{
return "dtse";
}
// Default to DTS core if profile is invalid
return "dtsc";
}
/// <summary> /// <summary>
/// Gets a H.264 codec string. /// Gets a H.264 codec string.
/// </summary> /// </summary>

View File

@@ -45,9 +45,15 @@ public static class HlsHelpers
using var reader = new StreamReader(fileStream); using var reader = new StreamReader(fileStream);
var count = 0; var count = 0;
string? line; while (!reader.EndOfStream)
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
{ {
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
if (line is null)
{
// Nothing currently in buffer.
break;
}
if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase)) if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase))
{ {
count++; count++;

View File

@@ -201,7 +201,7 @@ public static class StreamingHelpers
state.OutputVideoCodec = state.Request.VideoCodec; state.OutputVideoCodec = state.Request.VideoCodec;
state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
encodingHelper.TryStreamCopy(state, encodingOptions); encodingHelper.TryStreamCopy(state);
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue) if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{ {

View File

@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -0,0 +1,53 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Middleware;
/// <summary>
/// Removes /emby and /mediabrowser from requested route.
/// </summary>
public class LegacyEmbyRouteRewriteMiddleware
{
private const string EmbyPath = "/emby";
private const string MediabrowserPath = "/mediabrowser";
private readonly RequestDelegate _next;
private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">The logger.</param>
public LegacyEmbyRouteRewriteMiddleware(
RequestDelegate next,
ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Executes the middleware action.
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
var localPath = httpContext.Request.Path.ToString();
if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = localPath[EmbyPath.Length..];
_logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
}
else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = localPath[MediabrowserPath.Length..];
_logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
}
await _next(httpContext).ConfigureAwait(false);
}
}

View File

@@ -1,49 +0,0 @@
namespace Jellyfin.Data.Enums;
/// <summary>
/// Activity log sorting options.
/// </summary>
public enum ActivityLogSortBy
{
/// <summary>
/// Sort by name.
/// </summary>
Name = 0,
/// <summary>
/// Sort by overview.
/// </summary>
Overiew = 1,
/// <summary>
/// Sort by short overview.
/// </summary>
ShortOverview = 2,
/// <summary>
/// Sort by type.
/// </summary>
Type = 3,
/*
/// <summary>
/// Sort by item name.
/// </summary>
Item = 4,
*/
/// <summary>
/// Sort by date.
/// </summary>
DateCreated = 5,
/// <summary>
/// Sort by username.
/// </summary>
Username = 6,
/// <summary>
/// Sort by severity.
/// </summary>
LogSeverity = 7
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -18,7 +18,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId> <PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.12.0</VersionPrefix> <VersionPrefix>10.11.4</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,7 @@
using System; using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Data.Queries;
namespace Jellyfin.Data.Queries
{
/// <summary> /// <summary>
/// A class representing a query to the activity logs. /// A class representing a query to the activity logs.
/// </summary> /// </summary>
@@ -20,49 +16,5 @@ public class ActivityLogQuery : PaginatedQuery
/// Gets or sets the minimum date to query for. /// Gets or sets the minimum date to query for.
/// </summary> /// </summary>
public DateTime? MinDate { get; set; } public DateTime? MinDate { get; set; }
}
/// <summary>
/// Gets or sets the maximum date to query for.
/// </summary>
public DateTime? MaxDate { get; set; }
/// <summary>
/// Gets or sets the name filter.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the overview filter.
/// </summary>
public string? Overview { get; set; }
/// <summary>
/// Gets or sets the short overview filter.
/// </summary>
public string? ShortOverview { get; set; }
/// <summary>
/// Gets or sets the type filter.
/// </summary>
public string? Type { get; set; }
/// <summary>
/// Gets or sets the item filter.
/// </summary>
public Guid? ItemId { get; set; }
/// <summary>
/// Gets or sets the username filter.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Gets or sets the log level filter.
/// </summary>
public LogLevel? Severity { get; set; }
/// <summary>
/// Gets or sets the result ordering.
/// </summary>
public IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? OrderBy { get; set; }
} }

View File

@@ -1,21 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using Jellyfin.Data.Queries; using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Activity; namespace Jellyfin.Server.Implementations.Activity
{
/// <summary> /// <summary>
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances. /// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
/// </summary> /// </summary>
@@ -51,82 +46,28 @@ public class ActivityManager : IActivityManager
/// <inheritdoc/> /// <inheritdoc/>
public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query) public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
{ {
// TODO allow sorting and filtering by item id. Currently not possible because ActivityLog stores the item id as a string.
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
// TODO switch to LeftJoin in .NET 10. var entries = dbContext.ActivityLogs
var entries = from a in dbContext.ActivityLogs .OrderByDescending(entry => entry.DateCreated)
join u in dbContext.Users on a.UserId equals u.Id into ugj .Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate)
from u in ugj.DefaultIfEmpty() .Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value);
select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
if (query.HasUserId is not null)
{
entries = entries.Where(e => e.ActivityLog.UserId.Equals(default) != query.HasUserId.Value);
}
if (query.MinDate is not null)
{
entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value);
}
if (query.MaxDate is not null)
{
entries = entries.Where(e => e.ActivityLog.DateCreated <= query.MaxDate.Value);
}
if (!string.IsNullOrEmpty(query.Name))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%"));
}
if (!string.IsNullOrEmpty(query.Overview))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Overview, $"%{query.Overview}%"));
}
if (!string.IsNullOrEmpty(query.ShortOverview))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.ShortOverview, $"%{query.ShortOverview}%"));
}
if (!string.IsNullOrEmpty(query.Type))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Type, $"%{query.Type}%"));
}
if (!query.ItemId.IsNullOrEmpty())
{
var itemId = query.ItemId.Value.ToString("N");
entries = entries.Where(e => e.ActivityLog.ItemId == itemId);
}
if (!string.IsNullOrEmpty(query.Username))
{
entries = entries.Where(e => EF.Functions.Like(e.Username, $"%{query.Username}%"));
}
if (query.Severity is not null)
{
entries = entries.Where(e => e.ActivityLog.LogSeverity == query.Severity);
}
return new QueryResult<ActivityLogEntry>( return new QueryResult<ActivityLogEntry>(
query.Skip, query.Skip,
await entries.CountAsync().ConfigureAwait(false), await entries.CountAsync().ConfigureAwait(false),
await ApplyOrdering(entries, query.OrderBy) await entries
.Skip(query.Skip ?? 0) .Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100) .Take(query.Limit ?? 100)
.Select(entity => new ActivityLogEntry(entity.ActivityLog.Name, entity.ActivityLog.Type, entity.ActivityLog.UserId) .Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId)
{ {
Id = entity.ActivityLog.Id, Id = entity.Id,
Overview = entity.ActivityLog.Overview, Overview = entity.Overview,
ShortOverview = entity.ActivityLog.ShortOverview, ShortOverview = entity.ShortOverview,
ItemId = entity.ActivityLog.ItemId, ItemId = entity.ItemId,
Date = entity.ActivityLog.DateCreated, Date = entity.DateCreated,
Severity = entity.ActivityLog.LogSeverity Severity = entity.LogSeverity
}) })
.ToListAsync() .ToListAsync()
.ConfigureAwait(false)); .ConfigureAwait(false));
@@ -158,56 +99,5 @@ public class ActivityManager : IActivityManager
Severity = entry.LogSeverity Severity = entry.LogSeverity
}; };
} }
private IOrderedQueryable<ExpandedActivityLog> ApplyOrdering(IQueryable<ExpandedActivityLog> query, IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? sorting)
{
if (sorting is null || sorting.Count == 0)
{
return query.OrderByDescending(e => e.ActivityLog.DateCreated);
}
IOrderedQueryable<ExpandedActivityLog> ordered = null!;
foreach (var (sortBy, sortOrder) in sorting)
{
var orderBy = MapOrderBy(sortBy);
if (ordered == null)
{
ordered = sortOrder == SortOrder.Ascending
? query.OrderBy(orderBy)
: query.OrderByDescending(orderBy);
}
else
{
ordered = sortOrder == SortOrder.Ascending
? ordered.ThenBy(orderBy)
: ordered.ThenByDescending(orderBy);
}
}
return ordered;
}
private Expression<Func<ExpandedActivityLog, object?>> MapOrderBy(ActivityLogSortBy sortBy)
{
return sortBy switch
{
ActivityLogSortBy.Name => e => e.ActivityLog.Name,
ActivityLogSortBy.Overiew => e => e.ActivityLog.Overview,
ActivityLogSortBy.ShortOverview => e => e.ActivityLog.ShortOverview,
ActivityLogSortBy.Type => e => e.ActivityLog.Type,
ActivityLogSortBy.DateCreated => e => e.ActivityLog.DateCreated,
ActivityLogSortBy.Username => e => e.Username,
ActivityLogSortBy.LogSeverity => e => e.ActivityLog.LogSeverity,
_ => throw new ArgumentOutOfRangeException(nameof(sortBy), sortBy, "Unhandled ActivityLogSortBy")
};
}
private class ExpandedActivityLog
{
public ActivityLog ActivityLog { get; set; } = null!;
public string? Username { get; set; }
} }
} }

View File

@@ -158,7 +158,7 @@ namespace Jellyfin.Server.Implementations.Devices
devices = devices.Skip(query.Skip.Value); devices = devices.Skip(query.Skip.Value);
} }
if (query.Limit.HasValue && query.Limit.Value > 0) if (query.Limit.HasValue)
{ {
devices = devices.Take(query.Limit.Value); devices = devices.Take(query.Limit.Value);
} }

View File

@@ -102,7 +102,7 @@ public class BackupService : IBackupService
} }
BackupManifest? manifest; BackupManifest? manifest;
var manifestStream = await zipArchiveEntry.OpenAsync().ConfigureAwait(false); var manifestStream = zipArchiveEntry.Open();
await using (manifestStream.ConfigureAwait(false)) await using (manifestStream.ConfigureAwait(false))
{ {
manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false); manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
@@ -160,7 +160,7 @@ public class BackupService : IBackupService
} }
HistoryRow[] historyEntries; HistoryRow[] historyEntries;
var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false); var historyArchive = historyEntry.Open();
await using (historyArchive.ConfigureAwait(false)) await using (historyArchive.ConfigureAwait(false))
{ {
historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ?? historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ??
@@ -204,7 +204,7 @@ public class BackupService : IBackupService
continue; continue;
} }
var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false); var zipEntryStream = zipEntry.Open();
await using (zipEntryStream.ConfigureAwait(false)) await using (zipEntryStream.ConfigureAwait(false))
{ {
_logger.LogInformation("Restore backup of {Table}", entityType.Type.Name); _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
@@ -329,7 +329,7 @@ public class BackupService : IBackupService
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName); _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json"))); var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
var entities = 0; var entities = 0;
var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false); var zipEntryStream = zipEntry.Open();
await using (zipEntryStream.ConfigureAwait(false)) await using (zipEntryStream.ConfigureAwait(false))
{ {
var jsonSerializer = new Utf8JsonWriter(zipEntryStream); var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
@@ -366,7 +366,7 @@ public class BackupService : IBackupService
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)))
{ {
await zipArchive.CreateEntryFromFileAsync(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item)))).ConfigureAwait(false); zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
} }
void CopyDirectory(string source, string target, string filter = "*") void CopyDirectory(string source, string target, string filter = "*")
@@ -380,7 +380,6 @@ public class BackupService : IBackupService
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories)) foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
{ {
// TODO: @bond make async
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item)))); zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
} }
} }
@@ -406,7 +405,7 @@ public class BackupService : IBackupService
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
} }
var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false); var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
await using (manifestStream.ConfigureAwait(false)) await using (manifestStream.ConfigureAwait(false))
{ {
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false); await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
@@ -506,7 +505,7 @@ public class BackupService : IBackupService
return null; return null;
} }
var manifestStream = await manifestEntry.OpenAsync().ConfigureAwait(false); var manifestStream = manifestEntry.Open();
await using (manifestStream.ConfigureAwait(false)) await using (manifestStream.ConfigureAwait(false))
{ {
return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);

View File

@@ -250,7 +250,7 @@ public sealed class BaseItemRepository
public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter) public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
{ {
ArgumentNullException.ThrowIfNull(filter); ArgumentNullException.ThrowIfNull(filter);
if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0)) if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
{ {
var returnList = GetItemList(filter); var returnList = GetItemList(filter);
return new QueryResult<BaseItemDto>( return new QueryResult<BaseItemDto>(
@@ -275,9 +275,8 @@ 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)).Where(dto => dto is not null).ToArray()!; result.Items = GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
result.StartIndex = filter.StartIndex ?? 0; result.StartIndex = filter.StartIndex ?? 0;
return result; return result;
} }
@@ -295,9 +294,8 @@ 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)).Where(dto => dto is not null).ToArray()!; return GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -326,7 +324,7 @@ public sealed class BaseItemRepository
.OrderByDescending(g => g.MaxDateCreated) .OrderByDescending(g => g.MaxDateCreated)
.Select(g => g); .Select(g => g);
if (filter.Limit.HasValue && filter.Limit.Value > 0) if (filter.Limit.HasValue)
{ {
subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value); subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
} }
@@ -339,9 +337,7 @@ 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 GetEntities(mainquery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -367,7 +363,7 @@ public sealed class BaseItemRepository
.OrderByDescending(g => g.LastPlayedDate) .OrderByDescending(g => g.LastPlayedDate)
.Select(g => g.Key!); .Select(g => g.Key!);
if (filter.Limit.HasValue && filter.Limit.Value > 0) if (filter.Limit.HasValue)
{ {
query = query.Take(filter.Limit.Value); query = query.Take(filter.Limit.Value);
} }
@@ -408,47 +404,22 @@ public sealed class BaseItemRepository
return dbQuery; return dbQuery;
} }
private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
{
dbQuery = dbQuery.Include(e => e.TrailerTypes);
}
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)
{
dbQuery = dbQuery.Include(e => e.Images);
}
return dbQuery;
}
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{ {
if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) if (filter.Limit.HasValue || filter.StartIndex.HasValue)
{ {
dbQuery = dbQuery.Skip(filter.StartIndex.Value); var offset = filter.StartIndex ?? 0;
if (offset > 0)
{
dbQuery = dbQuery.Skip(offset);
} }
if (filter.Limit.HasValue && filter.Limit.Value > 0) if (filter.Limit.HasValue)
{ {
dbQuery = dbQuery.Take(filter.Limit.Value); dbQuery = dbQuery.Take(filter.Limit.Value);
} }
}
return dbQuery; return dbQuery;
} }
@@ -458,18 +429,50 @@ 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;
} }
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter) private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
{ {
IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking(); IQueryable<BaseItemEntity> dbQuery = context.BaseItems;
dbQuery = dbQuery.AsSingleQuery(); dbQuery = dbQuery.AsSingleQuery();
return dbQuery; return dbQuery;
} }
private IReadOnlyList<BaseItemEntity> GetEntities(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
{
var items = dbQuery.AsEnumerable().Where(e => e is not null).ToArray();
var itemIds = items.Select(e => e.Id).ToArray();
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
{
context.BaseItemTrailerTypes.WhereOneOrMany(itemIds, e => e.ItemId).Load();
}
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
{
context.BaseItemProviders.WhereOneOrMany(itemIds, e => e.ItemId).Load();
}
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
{
context.BaseItemMetadataFields.WhereOneOrMany(itemIds, e => e.ItemId).Load();
}
if (filter.DtoOptions.EnableImages)
{
context.BaseItemImageInfos.WhereOneOrMany(itemIds, e => e.ItemId).Load();
}
if (filter.DtoOptions.EnableUserData)
{
context.UserData.WhereOneOrMany(itemIds, e => e.ItemId).Load();
}
return items;
}
/// <inheritdoc/> /// <inheritdoc/>
public int GetCount(InternalItemsQuery filter) public int GetCount(InternalItemsQuery filter)
{ {
@@ -562,34 +565,22 @@ public sealed class BaseItemRepository
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default) public void SaveImages(BaseItemDto item)
{ {
ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(item);
var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray(); var images = item.ImageInfos.Select(e => Map(item.Id, e));
using var context = _dbProvider.CreateDbContext();
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); if (!context.BaseItems.Any(bi => bi.Id == item.Id))
await using (context.ConfigureAwait(false))
{
if (!await context.BaseItems
.AnyAsync(bi => bi.Id == item.Id, cancellationToken)
.ConfigureAwait(false))
{ {
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
return; return;
} }
await context.BaseItemImageInfos context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
.Where(e => e.ItemId == item.Id) context.BaseItemImageInfos.AddRange(images);
.ExecuteDeleteAsync(cancellationToken) context.SaveChanges();
.ConfigureAwait(false);
await context.BaseItemImageInfos
.AddRangeAsync(images, cancellationToken)
.ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -624,6 +615,7 @@ 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)
{ {
@@ -657,6 +649,19 @@ public sealed class BaseItemRepository
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();
@@ -752,29 +757,6 @@ 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)
{ {
@@ -882,7 +864,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('|', StringSplitOptions.RemoveEmptyEntries) ?? []; dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
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('|');
@@ -1044,7 +1026,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.Where(p => !string.IsNullOrWhiteSpace(p))) : null; entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : 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
@@ -1168,7 +1150,7 @@ public sealed class BaseItemRepository
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null; return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
} }
private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false) private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
{ {
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity)); ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
if (_serverConfigurationManager?.Configuration is null) if (_serverConfigurationManager?.Configuration is null)
@@ -1191,19 +1173,11 @@ public sealed class BaseItemRepository
/// <param name="logger">Logger.</param> /// <param name="logger">Logger.</param>
/// <param name="appHost">The application server Host.</param> /// <param name="appHost">The application server Host.</param>
/// <param name="skipDeserialization">If only mapping should be processed.</param> /// <param name="skipDeserialization">If only mapping should be processed.</param>
/// <returns>A mapped BaseItem, or null if the item type is unknown.</returns> /// <returns>A mapped BaseItem.</returns>
public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false) /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
{ {
var type = GetType(baseItemEntity.Type); var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
if (type is null)
{
logger.LogWarning(
"Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database corruption.",
baseItemEntity.Id,
baseItemEntity.Type);
return null;
}
BaseItemDto? dto = null; BaseItemDto? dto = null;
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization) if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
{ {
@@ -1229,7 +1203,7 @@ public sealed class BaseItemRepository
{ {
ArgumentNullException.ThrowIfNull(filter); ArgumentNullException.ThrowIfNull(filter);
if (!(filter.Limit.HasValue && filter.Limit.Value > 0)) if (!filter.Limit.HasValue)
{ {
filter.EnableTotalRecordCount = false; filter.EnableTotalRecordCount = false;
} }
@@ -1308,15 +1282,20 @@ public sealed class BaseItemRepository
result.TotalRecordCount = query.Count(); result.TotalRecordCount = query.Count();
} }
if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) if (filter.Limit.HasValue || filter.StartIndex.HasValue)
{ {
query = query.Skip(filter.StartIndex.Value); var offset = filter.StartIndex ?? 0;
if (offset > 0)
{
query = query.Skip(offset);
} }
if (filter.Limit.HasValue && filter.Limit.Value > 0) if (filter.Limit.HasValue)
{ {
query = query.Take(filter.Limit.Value); query = query.Take(filter.Limit.Value);
} }
}
IQueryable<BaseItemEntity>? itemCountQuery = null; IQueryable<BaseItemEntity>? itemCountQuery = null;
@@ -1370,9 +1349,10 @@ public sealed class BaseItemRepository
.. resultQuery .. resultQuery
.AsEnumerable() .AsEnumerable()
.Where(e => e is not null) .Where(e => e is not null)
.Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount)) .Select(e =>
.Where(e => e.Item is not null) {
.Select(e => (e.Item!, e.itemCount)) return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
})
]; ];
} }
else else
@@ -1383,9 +1363,10 @@ public sealed class BaseItemRepository
.. query .. query
.AsEnumerable() .AsEnumerable()
.Where(e => e is not null) .Where(e => e is not null)
.Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)null)) .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
.Where(e => e.Item is not null) {
.Select(e => (e.Item!, e.ItemCounts)) return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
})
]; ];
} }
@@ -1394,7 +1375,7 @@ public sealed class BaseItemRepository
private static void PrepareFilterQuery(InternalItemsQuery query) private static void PrepareFilterQuery(InternalItemsQuery query)
{ {
if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey) if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
{ {
query.Limit = query.Limit.Value + 4; query.Limit = query.Limit.Value + 4;
} }
@@ -1405,54 +1386,14 @@ public sealed class BaseItemRepository
} }
} }
/// <summary> private string GetCleanValue(string value)
/// Gets the clean value for search and sorting purposes.
/// </summary>
/// <param name="value">The value to clean.</param>
/// <returns>The cleaned value.</returns>
public static string GetCleanValue(string value)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
return value; return value;
} }
var noDiacritics = value.RemoveDiacritics(); return value.RemoveDiacritics().ToLowerInvariant();
// Build a string where any punctuation or symbol is treated as a separator (space).
var sb = new StringBuilder(noDiacritics.Length);
var previousWasSpace = false;
foreach (var ch in noDiacritics)
{
char outCh;
if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
{
outCh = ch;
}
else
{
outCh = ' ';
}
// normalize any whitespace character to a single ASCII space.
if (char.IsWhiteSpace(outCh))
{
if (!previousWasSpace)
{
sb.Append(' ');
previousWasSpace = true;
}
}
else
{
sb.Append(outCh);
previousWasSpace = false;
}
}
// trim leading/trailing spaces that may have been added.
var collapsed = sb.ToString().Trim();
return collapsed.ToLowerInvariant();
} }
private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags) private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
@@ -1615,36 +1556,29 @@ public sealed class BaseItemRepository
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, context); var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
if (orderedQuery is null) if (firstOrdering.SortOrder == SortOrder.Ascending)
{ {
// No search relevance ordering, start fresh orderedQuery = query.OrderBy(expression);
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
? query.OrderBy(expression)
: query.OrderByDescending(expression);
} }
else else
{ {
// Search relevance ordering already applied, chain with ThenBy orderedQuery = query.OrderByDescending(expression);
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)
{ {
orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending if (firstOrdering.SortOrder is SortOrder.Ascending)
? orderedQuery.ThenBy(e => e.Name) {
: orderedQuery.ThenByDescending(e => e.Name); orderedQuery = orderedQuery.ThenBy(e => e.Name);
}
else
{
orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
}
} }
} }
@@ -2511,24 +2445,35 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0) if (filter.ExcludeInheritedTags.Length > 0)
{ {
var excludedTags = filter.ExcludeInheritedTags;
baseQuery = baseQuery.Where(e => baseQuery = baseQuery.Where(e =>
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue)) !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.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)))); && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
!context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))));
} }
if (filter.IncludeInheritedTags.Length > 0) if (filter.IncludeInheritedTags.Length > 0)
{ {
var includeTags = filter.IncludeInheritedTags;
var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
baseQuery = baseQuery.Where(e =>
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
// For seasons and episodes, we also need to check the parent series' tags. // For seasons and episodes, we also need to check the parent series' tags.
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))) if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
{
baseQuery = baseQuery.Where(e =>
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
}
// A playlist should be accessible to its owner regardless of allowed tags // A playlist should be accessible to its owner regardless of allowed tags.
|| (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""))); else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
{
baseQuery = baseQuery.Where(e =>
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && 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!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
} }
if (filter.SeriesStatuses.Length > 0) if (filter.SeriesStatuses.Length > 0)
@@ -2682,6 +2627,6 @@ 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)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray()); return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
} }
} }

View File

@@ -158,12 +158,6 @@ public class MediaStreamRepository : IMediaStreamRepository
dto.LocalizedDefault = _localization.GetLocalizedString("Default"); dto.LocalizedDefault = _localization.GetLocalizedString("Default");
dto.LocalizedExternal = _localization.GetLocalizedString("External"); dto.LocalizedExternal = _localization.GetLocalizedString("External");
if (!string.IsNullOrEmpty(dto.Language))
{
var culture = _localization.FindLanguageInfo(dto.Language);
dto.LocalizedLanguage = culture?.DisplayName;
}
if (dto.Type is MediaStreamType.Subtitle) if (dto.Type is MediaStreamType.Subtitle)
{ {
dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");

View File

@@ -6,7 +6,6 @@ using System.Linq.Expressions;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations; 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;
@@ -69,30 +68,4 @@ public static class OrderMapper
_ => 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

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
@@ -27,6 +27,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AsyncKeyedLock" /> <PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
</ItemGroup> </ItemGroup>

View File

@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
} }
// As long as jellyfin supports password-less users, we need this little block here to accommodate // As long as jellyfin supports password-less users, we need this little block here to accommodate
if (string.IsNullOrEmpty(resolvedUser.Password) && string.IsNullOrEmpty(password)) if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
{ {
return Task.FromResult(new ProviderAuthenticationResult return Task.FromResult(new ProviderAuthenticationResult
{ {
@@ -93,6 +93,10 @@ namespace Jellyfin.Server.Implementations.Users
}); });
} }
/// <inheritdoc />
public bool HasPassword(User user)
=> !string.IsNullOrEmpty(user?.Password);
/// <inheritdoc /> /// <inheritdoc />
public Task ChangePassword(User user, string newPassword) public Task ChangePassword(User user, string newPassword)
{ {

View File

@@ -21,6 +21,12 @@ namespace Jellyfin.Server.Implementations.Users
throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
} }
/// <inheritdoc />
public bool HasPassword(User user)
{
return true;
}
/// <inheritdoc /> /// <inheritdoc />
public Task ChangePassword(User user, string newPassword) public Task ChangePassword(User user, string newPassword)
{ {

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