Compare commits

...

189 Commits

Author SHA1 Message Date
João Moura 12a2f7c1a5 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2025-12-16 18:22:40 +00:00
Joshua M. Boniface ebf8220f1d
Merge pull request #15798 from theguymadmax/10.11.5-template-version
Update issue template version to 10.11.5
2025-12-15 09:59:51 -05:00
theguymadmax c4e8180b3c Update issue template version to 10.11.5 2025-12-15 09:20:41 -05:00
Luigi311 771b0a7eab
Library: Async the SaveImages function (#15718) 2025-12-13 08:43:49 -07:00
renovate[bot] 4db0ab0f40
Update GitHub Artifact Actions (#15783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 08:42:57 -07:00
dkanada dd480f96cd
parse more information from book filenames (#15655) 2025-12-13 08:29:28 -07:00
Niels van Velzen 6b6d54a07c
Remove legacy API route middleware (#15669) 2025-12-13 08:26:22 -07:00
Bond-009 428beda1c7
Merge pull request #15780 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.31.8
2025-12-13 10:43:57 +01:00
Bond-009 baa8d40940
Merge pull request #15774 from stevenaw/optimize-GetUniqueFlags
Optimize GetUniqueFlags<T>()
2025-12-12 18:00:25 +01:00
renovate[bot] c8bdee26b7
Update github/codeql-action action to v4.31.8 2025-12-12 12:13:35 +00:00
stevenaw ef73ed6ef7 optimize GetUniqueFlags() 2025-12-11 22:15:19 -05:00
Veldermon-rbg d70e0fe9cf Translated using Weblate (Maori)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mi/
2025-12-10 05:20:53 +00:00
Veldermon-rbg 053cc9406d Added translation using Weblate (Maori) 2025-12-10 00:29:33 +00:00
Bas 0f85120c5e Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2025-12-10 00:29:32 +00:00
Tom O'Neill 25aef7fabf Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2025-12-10 00:29:32 +00:00
Mark Cilia Vincenti 492ea66841
Proper pinning of SkiaSharp to prevent accidental updates (#15736) 2025-12-08 21:16:14 -07:00
evan314159 8b2a8b94b6
avoid Take(0) when limit == 0 (#14608)
Co-authored-by: Evan <evan@MacBook-Pro.local>
2025-12-08 21:15:46 -07:00
xin f24e80701c
Fix typo in CheckOrCreateMarker exception (#15341) 2025-12-08 21:06:17 -07:00
Cody Robibero 0b3d6676d1
Add ability to sort and filter activity log entries (#15583) 2025-12-08 21:01:32 -07:00
Mark Cilia Vincenti c3a8734adf
Locking cleaning (#15713) 2025-12-08 21:01:12 -07:00
audrey-inglish 8fd59d6f33
Merge pull request #14879 from audrey-inglish/master
Fix: normalize punctuation when computing CleanName so searches without punctuation match (closes #1674)
2025-12-08 18:43:37 +01:00
Bond-009 da3bff3edf
Merge pull request #15433 from theguymadmax/fix-recently-added-shows
Fix episodes showing up on recently added shows
2025-12-08 18:38:50 +01:00
Bond-009 45cb5a0008
Merge pull request #15704 from theguymadmax/add-cpu-to-template
Add CPU to issue template
2025-12-05 20:50:59 +01:00
Bond-009 ea097fb1a3
Merge pull request #15706 from jellyfin/renovate/ci-deps
Update CI dependencies
2025-12-05 20:47:46 +01:00
renovate[bot] a25b48b151
Update CI dependencies 2025-12-05 18:58:06 +00:00
Furqaan Dawood 873f1d9e83 Added translation using Weblate (Swahili) 2025-12-04 14:41:47 +00:00
Bond-009 294439bf74
Merge pull request #15682 from jellyfin/renovate/ci-deps
Update CI dependencies
2025-12-03 20:05:29 +01:00
crobibero 6e74be0d46 Backport pull request #15672 from jellyfin/release-10.11.z
Cache OpenApi document generation

Original-merge: 8cd5652157

Merged-by: anthonylavado <anthony@lavado.ca>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:28 -05:00
nyanmisaka deb81eae10 Backport pull request #15670 from jellyfin/release-10.11.z
Fix the empty output of trickplay on RK3576

Original-merge: 98d1d0cb35

Merged-by: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:27 -05:00
theguymadmax 70dcf3f7b3 Backport pull request #15594 from jellyfin/release-10.11.z
Fix isMovie filter logic

Original-merge: 94f3725208

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:25 -05:00
QuintonQu ebcfed83c4 Backport pull request #15582 from jellyfin/release-10.11.z
Add hidden file check in BdInfoDirectoryInfo.cs.

Original-merge: 29b3aa8543

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:24 -05:00
theguymadmax 5d46278584 Backport pull request #15568 from jellyfin/release-10.11.z
Fix ResolveLinkTarget crashing on exFAT drives

Original-merge: fbb9a0b2c7

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:23 -05:00
theguymadmax 4f020a947a Backport pull request #15564 from jellyfin/release-10.11.z
Fix locked fields not saving

Original-merge: 0ee81e87be

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:22 -05:00
theguymadmax 3460d1de3c Backport pull request #15563 from jellyfin/release-10.11.z
Save item to database before providers run to prevent FK errors

Original-merge: c491a918c2

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:20 -05:00
gnattu 7d2e4cd817 Backport pull request #15557 from jellyfin/release-10.11.z
Restrict first video frame probing to file protocol

Original-merge: ee7ad83427

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:19 -05:00
gnattu 8cd6ef37c4 Backport pull request #15556 from jellyfin/release-10.11.z
Prevent copying HDR streams when only SDR is supported

Original-merge: 1e7e46cb82

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:18 -05:00
theguymadmax e4daaf0d83 Backport pull request #15548 from jellyfin/release-10.11.z
Fix NullReferenceException in filesystem path comparison

Original-merge: 5ae444d96d

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-12-03 14:04:17 -05:00
theguymadmax 69c98af9f9 Add CPU to issue template 2025-12-03 09:45:50 -05:00
renovate[bot] 7425a493ee
Update CI dependencies 2025-12-03 05:30:25 +00:00
Hasan Abdulaal 691c194152 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2025-12-02 13:27:39 +00:00
Niels van Velzen 2f8896c375
Merge pull request #15538 from KarkaLT/master
Add subtitle extraction timeout configuration option
2025-12-02 13:50:42 +01:00
Niels van Velzen 6c507b77ae
Remove DtoExtensions.AddClientFields (#15638) 2025-11-30 07:22:54 -07:00
renovate[bot] 6ed0ccd37c
Update appleboy/ssh-action action to v1.2.4 (#15660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 07:15:08 -07:00
Martín 80e1e42947 Added translation using Weblate (Occitan) 2025-11-28 20:01:20 +00:00
Niels van Velzen 6ace00eb6a
Merge pull request #15227 from kevgrig/issue15226
Add milliseconds to default console output format
2025-11-27 16:33:38 +01:00
Niels van Velzen a35ffbf17e
Merge pull request #13977 from sususu98/fix/strm-local-subtitle-url
refactor(StreamInfo): reorganize subtitle URL logic and conditions
2025-11-27 16:33:19 +01:00
Niels van Velzen 8c02c3be93
Merge pull request #14824 from CodyEngel/fix-numeric-titles
Fix TV Series parsing containing only numbers.
2025-11-27 16:32:11 +01:00
Niels van Velzen 45669c9b30
Merge pull request #15437 from allmazz/feat/more_file_metadata_tags
Add support for more embedded metadata tags
2025-11-27 16:31:42 +01:00
Niels van Velzen 19c232809e
Merge pull request #14950 from nielsvanvelzen/security-remove-has-password
Deprecate HasPassword property on UserDto
2025-11-27 16:31:05 +01:00
Niels van Velzen 301f65af48
Merge pull request #15559 from nielsvanvelzen/disable-legacy-auth
Disable legacy authorization methods by default
2025-11-27 16:30:45 +01:00
Bond-009 082ba58e51
Merge pull request #15630 from jellyfin/renovate/ci-deps
Update CI dependencies
2025-11-25 18:30:46 +01:00
Bond-009 3b5bdc6bc2
Merge pull request #15246 from JPVenson/feature/AddVersionDisplayStartupUi
Add version to StartupUI
2025-11-25 18:30:27 +01:00
Bond-009 b05e91dba1
Merge pull request #15614 from jellyfin/renovate/polly-monorepo
Update dependency Polly to 8.6.5
2025-11-25 18:26:41 +01:00
renovate[bot] c7703242e5
Update CI dependencies 2025-11-25 06:39:50 +00:00
Bond-009 21042ad0c2
Merge pull request #15626 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.31.5
2025-11-24 19:16:42 +01:00
renovate[bot] 8904551a59
Update github/codeql-action action to v4.31.5 2025-11-24 11:12:00 +00:00
renovate[bot] cf1ef22367
Update dependency Polly to 8.6.5 2025-11-23 14:03:55 +00:00
rimasx c08e81c52b Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/
2025-11-23 10:43:48 +00:00
Bond-009 23e66ae1ea
Merge pull request #15607 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.9.0
2025-11-23 10:22:43 +01:00
renovate[bot] 37bbdf3fe7
Update dependency z440.atl.core to 7.9.0 2025-11-22 15:15:12 +00:00
Bond-009 f124223015
Merge pull request #15591 from jellyfin/renovate/actions-checkout-6.x
Update actions/checkout action to v6
2025-11-22 10:22:11 +01:00
Bond-009 9587a9b13c
Merge pull request #15566 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.31.4
2025-11-22 10:20:48 +01:00
Niels van Velzen 67c67df507 Use async migration 2025-11-20 22:11:55 +01:00
renovate[bot] 569f8cfcfc
Update actions/checkout action to v6 2025-11-20 18:58:53 +00:00
Anthony Lavado aa4ddd139a
Add all 10.11 versions to issue template (#15565) 2025-11-18 21:05:43 -05:00
renovate[bot] 8ac97f5471
Update github/codeql-action action to v4.31.4 2025-11-19 01:37:24 +00:00
Bond-009 efabfbc931
Merge pull request #15542 from jellyfin/renovate/ci-deps
Update actions/checkout action to v5.0.1
2025-11-18 22:04:58 +01:00
Bond-009 6b5dc115e8
Merge pull request #15478 from jellyfin/renovate/microsoft
Update Microsoft
2025-11-18 22:01:56 +01:00
Bond-009 2dc0af667e
Merge pull request #15477 from jellyfin/renovate/dotnet-monorepo
Update dependency dotnet-ef to v9.0.11
2025-11-18 22:01:49 +01:00
Niels van Velzen 196c243a7d Disable legacy authorization methods by default 2025-11-18 16:17:04 +01:00
Rufis72 55dbff8f30 Translated using Weblate (English (Pirate))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en@pirate/
2025-11-18 08:19:02 +00:00
theguymadmax 2af43e0131 Backport pull request #15529 from jellyfin/release-10.11.z
Fix movie titles using folder name when NFO saver is enabled

Original-merge: f8e012582a

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:14 -05:00
theguymadmax faf1cea63e Backport pull request #15514 from jellyfin/release-10.11.z
Add 1 minute tolerance for NFO change detection

Original-merge: 6566188e45

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:13 -05:00
theguymadmax 7e25089c08 Backport pull request #15508 from jellyfin/release-10.11.z
Fix playlist DateCreated and DateLastMediaAdded not being set

Original-merge: 078f9584ed

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:12 -05:00
Iksas 8fa36a38e2 Backport pull request #15502 from jellyfin/release-10.11.z
Fix font extraction for certain transcoding settings

Original-merge: ee34c75386

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:10 -05:00
theguymadmax 5b3f29946b Backport pull request #15501 from jellyfin/release-10.11.z
Fix .ignore handling for directories

Original-merge: e8150428b6

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:09 -05:00
theguymadmax c869b5b884 Backport pull request #15493 from jellyfin/release-10.11.z
Remove InheritedTags and update tag filtering logic

Original-merge: 4b38e35bbb

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:08 -05:00
CBPJ a08b6ac266 Backport pull request #15487 from jellyfin/release-10.11.z
Fix gitignore-style not working properly on windows.

Original-merge: 435bb14bb2

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:07 -05:00
theguymadmax 4e68a5a078 Backport pull request #15472 from jellyfin/release-10.11.z
Fix series DateLastMediaAdded not updating when new episodes are added

Original-merge: abfbaca336

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:06 -05:00
Bond-009 99c68ddd50 Backport pull request #15468 from jellyfin/release-10.11.z
Check if target exists before trying to follow it

Original-merge: 5878b1ffc5

Merged-by: joshuaboniface <joshua@boniface.me>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:05 -05:00
Bond-009 d7f628677e Backport pull request #15466 from jellyfin/release-10.11.z
Don't error out when searching for marker files fails

Original-merge: f4a846aa4d

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:03 -05:00
theguymadmax e51680cf56 Backport pull request #15462 from jellyfin/release-10.11.z
Fix NullReferenceException in GetPathProtocol when path is null

Original-merge: 7c1063177f

Merged-by: joshuaboniface <joshua@boniface.me>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:02 -05:00
theguymadmax 2e7d7752e9 Backport pull request #15446 from jellyfin/release-10.11.z
Fix AncestorIds not migrating

Original-merge: 177b6464ca

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:09:01 -05:00
IceStormNG 26ac2ccd74 Backport pull request #15441 from jellyfin/release-10.11.z
Fix System.NullReferenceException when people's role is null (10.11.z)

Original-merge: 5a9a8363f4

Merged-by: Bond-009 <bond.009@outlook.com>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:59 -05:00
theguymadmax de9e653b73 Backport pull request #15435 from jellyfin/release-10.11.z
Fix search terms using diacritics

Original-merge: 63a3e55297

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:58 -05:00
theguymadmax e34e7a1d0b Backport pull request #15423 from jellyfin/release-10.11.z
Invalidate parent folder's cache on deletion/creation

Original-merge: 49efd68fc7

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:57 -05:00
nielsvanvelzen 5a30f108fe Backport pull request #15422 from jellyfin/release-10.11.z
Update branding in Swagger page

Original-merge: d140630208

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:56 -05:00
JPVenson 74c9629372 Backport pull request #15413 from jellyfin/release-10.11.z
Fixed missing sort argument

Original-merge: 91c3b1617e

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:55 -05:00
theguymadmax 6c5f448787 Backport pull request #15404 from jellyfin/release-10.11.z
Improve season folder parsing

Original-merge: 2e5ced5098

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:54 -05:00
Bond-009 f848b8f12c Backport pull request #15390 from jellyfin/release-10.11.z
Don't enforce a minimum amount of free space for the tmp and log dirs

Original-merge: 097cb87f6f

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:53 -05:00
theguymadmax bcec5f2e44 Backport pull request #15381 from jellyfin/release-10.11.z
Fix name filters to use only SortName

Original-merge: 7222910b05

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:51 -05:00
theguymadmax 7d05c875f3 Backport pull request #15380 from jellyfin/release-10.11.z
Fix item count display for collapsed items

Original-merge: 8f71922734

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:50 -05:00
theguymadmax c805c5e2b1 Backport pull request #15373 from jellyfin/release-10.11.z
Fix collection grouping in mixed libraries

Original-merge: 13c4517a66

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:49 -05:00
evanreichard c2c4c0adbf Backport pull request #15369 from jellyfin/release-10.11.z
feat(sqlite): add timeout config

Original-merge: c2e5081d64

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:48 -05:00
revam 5ea3910af9 Backport pull request #15263 from jellyfin/release-10.11.z
Resolve symlinks for static media source infos

Original-merge: 3b2d64995a

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:47 -05:00
theguymadmax 06fb300cff Backport pull request #14955 from jellyfin/release-10.11.z
Fix tmdbid not detected in single movie folder

Original-merge: def5956cd1

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-11-17 14:08:45 -05:00
renovate[bot] 626ab7e00a
Update actions/checkout action to v5.0.1 2025-11-17 18:40:00 +00:00
Bond-009 1d140645b0
Merge pull request #15528 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.8.0
2025-11-17 19:38:20 +01:00
Karolis 5182aec13f Add subtitle extraction timeout configuration option 2025-11-17 15:18:29 +02:00
renovate[bot] 52f0c3dd24
Update dependency z440.atl.core to 7.8.0 2025-11-16 17:53:55 +00:00
Bond-009 b8327dbc9f
Merge pull request #15480 from jellyfin/renovate/ci-deps
Update CI dependencies
2025-11-16 18:52:54 +01:00
hoanghuy309 d1722936c0 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/
2025-11-15 06:48:18 +00:00
renovate[bot] 931240a3f5
Update CI dependencies 2025-11-14 01:24:06 +00:00
Grant Alexander b216a27bfc Translated using Weblate (English (Pirate))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en@pirate/
2025-11-12 23:22:20 +00:00
renovate[bot] 8471a67bcd
Update Microsoft 2025-11-11 18:42:18 +00:00
renovate[bot] b8a409195f
Update dependency dotnet-ef to v9.0.11 2025-11-11 18:42:10 +00:00
Bond-009 1da67e5e10
Merge pull request #15450 from jellyfin/renovate/fscheck.xunit-3.x
Update dependency FsCheck.Xunit to 3.3.2
2025-11-09 19:39:59 +01:00
Bond-009 ed1ec7ca6b
Merge pull request #15448 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.7.0
2025-11-09 17:57:55 +01:00
renovate[bot] 3d7a68beb1
Update dependency FsCheck.Xunit to 3.3.2 2025-11-09 15:40:14 +00:00
renovate[bot] 32fc57cf17
Update dependency z440.atl.core to 7.7.0 2025-11-09 10:59:00 +00:00
Bond-009 0598c6eaf6
Merge pull request #15438 from jellyfin/renovate/ci-deps
Update appleboy/ssh-action action to v1.2.3
2025-11-08 17:17:52 +01:00
Andrew 0d7b687da0
Update Jellyfin Server version in issue template (#15398) 2025-11-08 08:30:30 -07:00
renovate[bot] e69754fd3a
Update appleboy/ssh-action action to v1.2.3 2025-11-08 04:18:07 +00:00
Kirill Nikiforov ac81ddd39a
add support for more embedded metadata tags 2025-11-08 02:54:53 +04:00
theguymadmax 217ea488df Fix episode showing up on recently added shows 2025-11-07 09:39:23 -05:00
Diogo Coelho f693c9d39f Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2025-11-06 11:37:15 +00:00
Bond-009 96d72788a1
Merge pull request #15312 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.31.2
2025-11-04 21:07:02 +01:00
Bond-009 0d74a95bb8
Merge pull request #15348 from jellyfin/renovate/serilog.sinks.console-6.x
Update dependency Serilog.Sinks.Console to 6.1.1
2025-11-04 21:05:17 +01:00
evanreichard a7d039b7c6 Backport pull request #15328 from jellyfin/release-10.11.z
fix: in optimistic locking, key off table is locked

Original-merge: b5f0199a25

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:58:46 -05:00
Shadowghost 87b02b1316 Backport pull request #15326 from jellyfin/release-10.11.z
Skip too large extracted season numbers

Original-merge: e7dbb3afec

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:58:45 -05:00
vinnyspb 871de372ff Backport pull request #15325 from jellyfin/release-10.11.z
Update file size when refreshing metadata

Original-merge: f994dd6211

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:58:44 -05:00
crobibero c9d93b0745 Backport pull request #15322 from jellyfin/release-10.11.z
Fix legacy migration file checks

Original-merge: da254ee968

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:58:43 -05:00
thornbill 1ccd10863e Backport pull request #15254 from jellyfin/release-10.11.z
Update password reset to always return the same response structure

Original-merge: 4ad3141875

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:58:42 -05:00
nyanmisaka 4258df4485 Backport pull request #15247 from jellyfin/release-10.11.z
Ignore initial delay in audio-only containers

Original-merge: 6bf88c049e

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:58:41 -05:00
renovate[bot] 63f06aad94
Update dependency Serilog.Sinks.Console to 6.1.1 2025-11-02 23:14:23 +00:00
Jacky He ffe82be7a7 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2025-10-31 23:39:28 +00:00
Jacky He 23929a3e70 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2025-10-30 19:34:52 +00:00
renovate[bot] 83d0dbdbcb
Update github/codeql-action action to v4.31.2 2025-10-30 18:35:55 +00:00
Battseren Badral 573ce9ceaa Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/
2025-10-29 22:47:08 +00:00
Jacky He f21fe9f95e Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2025-10-29 22:47:08 +00:00
Battseren Badral f92eca3efb Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/
2025-10-29 00:03:00 +00:00
Pascal Wiesmann 7d778d7bef Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/
2025-10-28 09:23:31 +00:00
JJBlue 21f65e2e27 Backport pull request #15220 from jellyfin/release-10.11.z
Skip extracted files in migration if bad timestamp or no access

Original-merge: a305204cfa

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:31 -04:00
theguymadmax 28b0657608 Backport pull request #15217 from jellyfin/release-10.11.z
Normalize paths in database queries

Original-merge: 75f472e6a7

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:30 -04:00
crobibero a489942454 Backport pull request #15212 from jellyfin/release-10.11.z
Skip invalid database migration

Original-merge: 2966d27c97

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:29 -04:00
Shadowghost 423c2654c0 Backport pull request #15209 from jellyfin/release-10.11.z
Improve symlink handling

Original-merge: e5656af1f2

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:27 -04:00
crobibero 4dc826644d Backport pull request #15197 from jellyfin/release-10.11.z
Filter plugins by id instead of name

Original-merge: 5691eee4f1

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:26 -04:00
crobibero 0f21222a0c Backport pull request #15196 from jellyfin/release-10.11.z
Skip directory entry when restoring from backup

Original-merge: 0e4031ae52

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:25 -04:00
crobibero 570b8b2eb9 Backport pull request #15194 from jellyfin/release-10.11.z
Initialize transcode marker during startup

Original-merge: 81b8b0ca4a

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:24 -04:00
Shadowghost 08fd175f5a Backport pull request #15187 from jellyfin/release-10.11.z
Fix pagination and sorting for folders

Original-merge: 7d1824ea27

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:23 -04:00
gnattu 511b5d9c53 Backport pull request #15177 from jellyfin/release-10.11.z
Make priority class setting more robust

Original-merge: 70c32a26fa

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:22 -04:00
CeruleanRed 6514196e8d Backport pull request #15176 from jellyfin/release-10.11.z
Only save chapters that are within the runtime of the video file

Original-merge: 442af96ed9

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:21 -04:00
crobibero ed6cb30762 Backport pull request #15170 from jellyfin/release-10.11.z
Clean up BackupService

Original-merge: ac3fa3c376

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:20 -04:00
crobibero 232c0399e2 Backport pull request #15164 from jellyfin/release-10.11.z
Fix XmlOutputFormatter

Original-merge: 2b94bb54aa

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:19 -04:00
nyanmisaka dbb015441f Backport pull request #15144 from jellyfin/release-10.11.z
Fix videos with cropping metadata are probed as anamorphic

Original-merge: 175ee12bbc

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:18 -04:00
theguymadmax 4c1c160990 Backport pull request #15133 from jellyfin/release-10.11.z
Play selected song first with instant mix

Original-merge: 1520a697ad

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:17 -04:00
MBR-0001 0931d6e4de Backport pull request #15126 from jellyfin/release-10.11.z
Fix Has(Imdb/Tmdb/Tvdb)Id checks

Original-merge: 14b3085ff1

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:15 -04:00
ivanjx 3f2ebc4179 Backport pull request #15113 from jellyfin/release-10.11.z
Add season number fallback for OMDB and TMDB plugins

Original-merge: 618ec4543e

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:14 -04:00
Shadowghost 14e8194581 Backport pull request #15112 from jellyfin/release-10.11.z
Skip extracted files in migration if bad timestamp or no access

Original-merge: 7a1c1cd342

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:13 -04:00
theguymadmax 3c4dc16003 Backport pull request #15102 from jellyfin/release-10.11.z
Make season paths case-insensitive

Original-merge: 305b0fdca3

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:12 -04:00
Bond-009 54d28d9842 Backport pull request #15098 from jellyfin/release-10.11.z
Lower required tmp dir size to 512MiB

Original-merge: 0a6e8146be

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:11 -04:00
theguymadmax adfa520057 Backport pull request #15087 from jellyfin/release-10.11.z
Optimize WhereReferencedItemMultipleTypes filtering

Original-merge: a5bc4524d8

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:10 -04:00
theguymadmax 5deb69b23f Backport pull request #15083 from jellyfin/release-10.11.z
Fix LiveTV images not saving to database

Original-merge: d738386fe2

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:09 -04:00
nyanmisaka 348b2992d7 Backport pull request #15072 from jellyfin/release-10.11.z
Reject stream copy of HDR10+ video if the client does not support HDR10

Original-merge: a725220c21

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:08 -04:00
gnattu 9f8fb6d588 Backport pull request #15055 from jellyfin/release-10.11.z
Log the message more clear when network manager is not ready

Original-merge: a245605152

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:07 -04:00
Shadowghost cee16d47cb Backport pull request #15054 from jellyfin/release-10.11.z
Speed-up trickplay migration

Original-merge: ca830d5be7

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:06 -04:00
Shadowghost 9e53f46ad2 Backport pull request #15032 from jellyfin/release-10.11.z
Skip invalid keyframe cache data

Original-merge: f4a53209f4

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:04 -04:00
Joshua M. Boniface 53dfcae1a6
Merge pull request #15236 from joshuaboniface/codeowners
Update CODEOWNERS to capture bump_version
2025-10-27 09:10:47 -04:00
JPVenson 81f1cc78b2 Add version to StartupUI 2025-10-27 13:01:52 +00:00
kreaxv efd659412f Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/
2025-10-27 09:41:00 +00:00
Joshua M. Boniface c31ea251c4 Improve handling of .github dir 2025-10-26 22:17:33 -04:00
Joshua M. Boniface 285e7c6c4f Update CODEOWNERS to capture bump_version 2025-10-26 22:07:46 -04:00
Joshua M. Boniface c274336563 Bump version to 10.12.0 (for real this time) 2025-10-26 21:52:03 -04:00
Joshua M. Boniface d5fd5dfe6a Fix bump_version to handle spaced filename 2025-10-26 21:51:41 -04:00
Kevin G 42ddcfa565
Add milliseconds to default console output format
Signed-off-by: Kevin G <kevin@myplaceonline.com>
2025-10-26 10:29:29 -05:00
Bond-009 6fa69f9fe5
Merge pull request #15206 from jellyfin/renovate/ci-deps
Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.18
2025-10-26 14:50:42 +01:00
Bond-009 0b876365a1
Merge pull request #15205 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.6.0
2025-10-26 14:39:09 +01:00
Battseren Badral cdc8325c7b Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/
2025-10-25 21:55:13 +00:00
renovate[bot] a6a8e29916
Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.18 2025-10-25 15:32:59 +00:00
renovate[bot] 6fd3847298
Update dependency z440.atl.core to 7.6.0 2025-10-25 15:12:02 +00:00
Bond-009 3ff516a430
Merge pull request #15191 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.31.0
2025-10-25 11:23:34 +02:00
Bond-009 d8591840f3
Merge pull request #15192 from jellyfin/renovate/major-github-artifact-actions
Update GitHub Artifact Actions (major)
2025-10-25 11:22:40 +02:00
HanHwanHo c5affbbf71 Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/
2025-10-25 07:46:36 +00:00
renovate[bot] 788f090f27
Update GitHub Artifact Actions 2025-10-24 23:59:16 +00:00
renovate[bot] 0e3b6652b3
Update github/codeql-action action to v4.31.0 2025-10-24 23:59:06 +00:00
desibooklover d167d59c23 Translated using Weblate (Punjabi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pa/
2025-10-23 03:50:04 +00:00
desibooklover f58b4860f7 Translated using Weblate (Hindi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/
2025-10-23 03:50:04 +00:00
Grant Alexander 96b7fc0ac0 Translated using Weblate (Spanish (Mexico))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_MX/
2025-10-21 15:20:51 +00:00
oddife c8ad861590 Translated using Weblate (Marathi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mr/
2025-10-20 21:27:58 +00:00
Jellyfin Release Bot 1a1a24cfff Bump version to 10.12.0 2025-10-19 20:45:13 -04:00
Niels van Velzen d43db230fa Add back UpdateUserPassword_Empty_RemoveSetPassword test 2025-10-19 09:45:55 +02:00
Niels van Velzen 0fb6d930e1 Deprecate HasPassword property on UserDto 2025-10-05 11:10:36 +02:00
Cody Engel 2508e8349b
update summary docs
Signed-off-by: Cody Engel <cengel815@gmail.com>
2025-09-23 08:22:00 -06:00
Cody Engel bd9a44ce7d
remove explicit ‘-‘ support in series name 2025-09-20 18:00:44 -06:00
Cody Engel da31d0c6a6
support series that are numeric only
updates SeriesResolver to handle series names that only contain numbers such as 1923.
2025-09-20 14:04:00 -06:00
sususu98 aebabb1580 style: fix return statement indentation in StreamInfo.cs 2025-04-24 14:25:12 +08:00
sususu98 d5402718b7
Merge branch 'jellyfin:master' into fix/strm-local-subtitle-url 2025-04-24 14:18:17 +08:00
sususu98 fd108ff528 Style: Fix indentation in StreamInfo.cs 2025-04-24 14:17:33 +08:00
sususu98 22ce1f25d0 refactor(StreamInfo): reorganize subtitle URL logic and conditions
# Conflicts:
#	MediaBrowser.Model/Dlna/StreamInfo.cs
2025-04-23 18:18:38 +08:00
151 changed files with 2212 additions and 1208 deletions

View File

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

15
.github/CODEOWNERS vendored
View File

@ -1,4 +1,11 @@
# Joshua must review all changes to deployment and build.sh # Joshua must review all changes to bump_version and any files it touches
.ci/* @joshuaboniface bump_version @joshuaboniface
deployment/* @joshuaboniface .github/ISSUE_TEMPLATE @joshuaboniface
build.sh @joshuaboniface MediaBrowser.Common/MediaBrowser.Common.csproj @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,7 +87,12 @@ 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.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*
@ -136,13 +141,14 @@ 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**: [e.g. local SATA SSD, local HDD] - **Jellyfin Data Storage & Filesystem**: [e.g. local SATA SSD - ext4, local HDD - NTFS]
- **Media Storage**: [e.g. Local HDD, SMB Share] - **Media Storage & Filesystem**: [e.g. Local HDD - ext4, SMB Share]
- **External Integrations**: [e.g. Jellystat, Jellyseerr] - **External Integrations**: [e.g. Jellystat, Jellyseerr]
value: | value: |
- OS: - OS:
@ -153,13 +159,14 @@ 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: - Jellyfin Data Storage & Filesystem:
- Media Storage: - Media Storage & Filesystem:
- External Integrations: - External Integrations:
render: markdown render: markdown
validations: validations:

View File

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

View File

@ -11,13 +11,13 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: abi-head name: abi-head
retention-days: 14 retention-days: 14
@ -40,14 +40,14 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: abi-base name: abi-base
retention-days: 14 retention-days: 14
@ -85,13 +85,13 @@ jobs:
steps: steps:
- name: Download abi-head - name: Download abi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: abi-head name: abi-head
path: abi-head path: abi-head
- name: Download abi-base - name: Download abi-base
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: abi-base name: abi-base
path: abi-base path: abi-base

View File

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

View File

@ -20,9 +20,9 @@ jobs:
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: ${{ env.SDK_VERSION }} dotnet-version: ${{ env.SDK_VERSION }}
@ -35,7 +35,7 @@ jobs:
--verbosity minimal --verbosity minimal
- name: Merge code coverage results - name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17 uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
with: with:
reports: "**/coverage.cobertura.xml" reports: "**/coverage.cobertura.xml"
targetdir: "merged/" targetdir: "merged/"

View File

@ -24,7 +24,7 @@ jobs:
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@ -40,11 +40,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.14' python-version: '3.14'
cache: 'pip' cache: 'pip'

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8 yq-version: v4.9.8
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}
@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}

View File

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

View File

@ -0,0 +1,75 @@
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

@ -0,0 +1,41 @@
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

@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.11.0</VersionPrefix> <VersionPrefix>10.12.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

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

View File

@ -17,6 +17,13 @@ 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>
@ -27,6 +34,20 @@ 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

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -11,28 +12,24 @@ namespace Emby.Server.Implementations.Library;
/// </summary> /// </summary>
public class DotIgnoreIgnoreRule : IResolverIgnoreRule public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{ {
private static readonly bool IsWindows = OperatingSystem.IsWindows();
private static FileInfo? FindIgnoreFile(DirectoryInfo directory) private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
{ {
var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); for (var current = directory; current is not null; current = current.Parent)
if (ignoreFile.Exists)
{ {
return ignoreFile; var ignorePath = Path.Join(current.FullName, ".ignore");
if (File.Exists(ignorePath))
{
return new FileInfo(ignorePath);
}
} }
var parentDir = directory.Parent; return null;
if (parentDir is null)
{
return null;
}
return FindIgnoreFile(parentDir);
} }
/// <inheritdoc /> /// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
{
return IsIgnored(fileInfo, parent);
}
/// <summary> /// <summary>
/// Checks whether or not the file is ignored. /// Checks whether or not the file is ignored.
@ -42,60 +39,58 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <returns>True if the file should be ignored.</returns> /// <returns>True if the file should be ignored.</returns>
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{ {
if (fileInfo.IsDirectory) var searchDirectory = fileInfo.IsDirectory
{ ? new DirectoryInfo(fileInfo.FullName)
var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName)); : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
if (dirIgnoreFile is null)
{
return false;
}
// Fast path in case the ignore files isn't a symlink and is empty if (string.IsNullOrEmpty(searchDirectory.FullName))
if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0
&& dirIgnoreFile.Length == 0)
{
return true;
}
// ignore the directory only if the .ignore file is empty
// evaluate individual files otherwise
return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
}
var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
if (string.IsNullOrEmpty(parentDirPath))
{ {
return false; return false;
} }
var folder = new DirectoryInfo(parentDirPath); var ignoreFile = FindIgnoreFile(searchDirectory);
var ignoreFile = FindIgnoreFile(folder);
if (ignoreFile is null) if (ignoreFile is null)
{ {
return false; return false;
} }
string ignoreFileString = GetFileContent(ignoreFile); // Fast path in case the ignore files isn't a symlink and is empty
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
if (string.IsNullOrWhiteSpace(ignoreFileString))
{ {
// Ignore directory if we just have the file // Ignore directory if we just have the file
return true; return true;
} }
// If file has content, base ignoring off the content .gitignore-style rules var content = GetFileContent(ignoreFile);
var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); return string.IsNullOrWhiteSpace(content)
var ignore = new Ignore.Ignore(); || CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
ignore.Add(ignoreRules);
return ignore.IsIgnored(fileInfo.FullName);
} }
private static string GetFileContent(FileInfo dirIgnoreFile) private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
{ {
using (var reader = dirIgnoreFile.OpenText()) // If file has content, base ignoring off the content .gitignore-style rules
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ignore = new Ignore.Ignore();
ignore.Add(rules);
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
// See https://github.com/jellyfin/jellyfin/issues/15484
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
// Add trailing slash for directories to match "folder/"
if (isDirectory)
{ {
return reader.ReadToEnd(); pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
} }
return ignore.IsIgnored(pathToCheck);
}
private static string GetFileContent(FileInfo ignoreFile)
{
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
return ignoreFile.Exists
? File.ReadAllText(ignoreFile.FullName)
: string.Empty;
} }
} }

View File

@ -457,6 +457,12 @@ namespace Emby.Server.Implementations.Library
_cache.TryRemove(child.Id, out _); _cache.TryRemove(child.Id, out _);
} }
if (parent is Folder folder)
{
folder.Children = null;
folder.UserData = null;
}
ReportItemRemoved(item, parent); ReportItemRemoved(item, parent);
} }
@ -1993,6 +1999,12 @@ namespace Emby.Server.Implementations.Library
RegisterItem(item); RegisterItem(item);
} }
if (parent is Folder folder)
{
folder.Children = null;
folder.UserData = null;
}
if (ItemAdded is not null) if (ItemAdded is not null)
{ {
foreach (var item in items) foreach (var item in items)
@ -2131,7 +2143,7 @@ namespace Emby.Server.Implementations.Library
item.ValidateImages(); item.ValidateImages();
_itemRepository.SaveImages(item); await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false);
RegisterItem(item); RegisterItem(item);
} }
@ -2150,6 +2162,12 @@ namespace Emby.Server.Implementations.Library
_itemRepository.SaveItems(items, cancellationToken); _itemRepository.SaveItems(items, cancellationToken);
if (parent is Folder folder)
{
folder.Children = null;
folder.UserData = null;
}
if (ItemUpdated is not null) if (ItemUpdated is not null)
{ {
foreach (var item in items) foreach (var item in items)

View File

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

View File

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

View File

@ -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,17 +35,22 @@ 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))
{ {
// It's a book return null;
return new Book
{
Path = args.Path,
IsInMixedFolder = true
};
} }
return null; var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path));
return new Book
{
Path = args.Path,
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
IsInMixedFolder = true,
};
} }
private Book GetBook(ItemResolveArgs args) private Book GetBook(ItemResolveArgs args)
@ -59,15 +64,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
StringComparison.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
}).ToList(); }).ToList();
// Don't return a Book if there is more (or less) than one document in the directory // directory is only considered a book when it contains exactly one supported file
// 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

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

View File

@ -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) if (query.Limit.HasValue && query.Limit.Value > 0)
{ {
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count)); results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
} }

View File

@ -16,7 +16,7 @@
"Folders": "المجلدات", "Folders": "المجلدات",
"Genres": "التصنيفات", "Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم", "HeaderAlbumArtists": "فناني الألبوم",
"HeaderContinueWatching": "إستئناف المشاهدة", "HeaderContinueWatching": "أكمل المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة", "HeaderFavoriteEpisodes": "الحلقات المفضلة",

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 de reproducción: {0}", "LabelRunningTimeValue": "Tiempo corriendo: {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

@ -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, mis pole enam vähemalt 90 päeva saadaval olnud." "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida 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": "Fehlgeschlagener Anmeldeversuch von {0}", "FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}",
"Favorites": "Favorite", "Favorites": "Favorite",
"Folders": "Ordner", "Folders": "Ordner",
"Genres": "Genre", "Genres": "Genre",

View File

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

View File

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

View File

@ -0,0 +1,9 @@
{
"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

@ -3,7 +3,7 @@
"HeaderNextUp": "Дараа нь", "HeaderNextUp": "Дараа нь",
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх", "HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
"Songs": "Дуунууд", "Songs": "Дуунууд",
"Playlists": "Playlist-ууд", "Playlists": "Тоглуулах жагсаалтууд",
"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,5 +132,10 @@
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा", "TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण", "TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.", "TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो" "TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
} }

View File

@ -0,0 +1 @@
{}

View File

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

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 get in, try from {0}", "FailedLoginAttemptWithUserName": "Ye failed to enter 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,5 +113,10 @@
"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 de câmara foi enviada a partir de {0}", "CameraImageUploadedFrom": "Uma nova imagem da 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 truques", "TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.", "TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas 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

@ -0,0 +1 @@
{}

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} đã phát xong {1} trên {2}", "UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {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,5 +137,6 @@
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。", "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
"CleanupUserDataTask": "用戶資料清理工作" "CleanupUserDataTask": "用戶資料清理工作",
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
} }

View File

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

View File

@ -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) if (query.Limit.HasValue && query.Limit.Value > 0)
{ {
items = items.Take(query.Limit.Value); items = items.Take(query.Limit.Value);
} }

View File

@ -223,15 +223,14 @@ namespace Emby.Server.Implementations.Updates
Guid id = default, Guid id = default,
Version? specificVersion = null) Version? specificVersion = null)
{ {
if (name is not null)
{
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (!id.IsEmpty()) if (!id.IsEmpty())
{ {
availablePackages = availablePackages.Where(x => x.Id.Equals(id)); availablePackages = availablePackages.Where(x => x.Id.Equals(id));
} }
else if (name is not null)
{
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (specificVersion is not null) if (specificVersion is not null)
{ {

View File

@ -1,13 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Data.Enums;
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;
@ -32,10 +35,19 @@ public class ActivityLogController : BaseJellyfinApiController
/// <summary> /// <summary>
/// Gets activity log entries. /// Gets activity log entries.
/// </summary> /// </summary>
/// <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="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">The maximum number of records to return.</param>
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param> /// <param name="minDate">The minimum 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")]
@ -44,14 +56,60 @@ public class ActivityLogController : BaseJellyfinApiController
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] DateTime? minDate, [FromQuery] DateTime? minDate,
[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)
{ {
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery var query = new ActivityLogQuery
{ {
Skip = startIndex, Skip = startIndex,
Limit = limit, Limit = limit,
MinDate = minDate, MinDate = minDate,
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,7 +122,6 @@ 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;
@ -326,7 +325,6 @@ 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;
@ -467,7 +465,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().AddClientFields(User); var dtoOptions = new DtoOptions();
var item = _libraryManager.GetArtist(name, dtoOptions); var item = _libraryManager.GetArtist(name, dtoOptions);

View File

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

View File

@ -1625,8 +1625,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions; var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT if (state.VideoStream is not null && state.IsOutputVideo)
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont"; {
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
}
segmentFormat = "fmp4" + outputFmp4HeaderArg; segmentFormat = "fmp4" + outputFmp4HeaderArg;
} }

View File

@ -94,7 +94,6 @@ 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()
@ -159,8 +158,7 @@ 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,7 +90,6 @@ 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);
@ -134,7 +133,6 @@ 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);
@ -178,7 +176,6 @@ 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);
@ -214,7 +211,6 @@ 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);
@ -258,7 +254,6 @@ 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);
@ -302,7 +297,6 @@ 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);
@ -385,7 +379,6 @@ 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

@ -268,7 +268,6 @@ 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
@ -849,7 +848,6 @@ 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

@ -187,7 +187,7 @@ public class LibraryController : BaseJellyfinApiController
item = parent; item = parent;
} }
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions();
var items = themeItems var items = themeItems
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
.ToArray(); .ToArray();
@ -260,7 +260,7 @@ public class LibraryController : BaseJellyfinApiController
item = parent; item = parent;
} }
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions();
var items = themeItems var items = themeItems
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
.ToArray(); .ToArray();
@ -496,7 +496,7 @@ public class LibraryController : BaseJellyfinApiController
var baseItemDtos = new List<BaseItemDto>(); var baseItemDtos = new List<BaseItemDto>();
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions();
BaseItem? parent = item.GetParent(); BaseItem? parent = item.GetParent();
while (parent is not null) while (parent is not null)
@ -556,7 +556,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().AddClientFields(User); var dtoOptions = new DtoOptions();
var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
return new QueryResult<BaseItemDto>(resultArray); return new QueryResult<BaseItemDto>(resultArray);
} }
@ -747,8 +747,7 @@ 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

@ -170,7 +170,6 @@ 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(
@ -242,8 +241,7 @@ 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);
} }
@ -297,7 +295,6 @@ 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(
@ -444,8 +441,7 @@ 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);
} }
@ -635,7 +631,6 @@ 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);
} }
@ -690,7 +685,6 @@ 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);
} }
@ -760,7 +754,6 @@ 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,8 +74,7 @@ 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,7 +94,6 @@ 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()
@ -148,7 +147,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().AddClientFields(User); var dtoOptions = new DtoOptions();
MusicGenre? item; MusicGenre? item;

View File

@ -81,7 +81,6 @@ 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()
@ -121,8 +120,7 @@ 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,7 +548,6 @@ 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,7 +89,6 @@ 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()
@ -142,7 +141,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().AddClientFields(User); var dtoOptions = new DtoOptions();
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().AddClientFields(User); var dtoOptions = new DtoOptions();
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,7 +99,6 @@ 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(
@ -161,7 +160,6 @@ 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)
@ -231,7 +229,6 @@ 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();
@ -360,7 +357,6 @@ 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

@ -13,6 +13,7 @@ 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;
@ -94,7 +95,7 @@ public class UserLibraryController : BaseJellyfinApiController
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions();
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
@ -133,7 +134,7 @@ public class UserLibraryController : BaseJellyfinApiController
} }
var item = _libraryManager.GetUserRootFolder(); var item = _libraryManager.GetUserRootFolder();
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions();
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
@ -180,7 +181,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().AddClientFields(User); var dtoOptions = new DtoOptions();
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);
@ -422,7 +423,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions();
if (item is IHasTrailers hasTrailers) if (item is IHasTrailers hasTrailers)
{ {
var trailers = hasTrailers.LocalTrailers; var trailers = hasTrailers.LocalTrailers;
@ -478,7 +479,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions();
return Ok(item return Ok(item
.GetExtras() .GetExtras()
@ -549,7 +550,6 @@ 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)) if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum || i.Item1 is Series ))
{ {
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().AddClientFields(User); var dtoOptions = new DtoOptions();
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,7 +111,6 @@ 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)

View File

@ -89,7 +89,6 @@ 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()
@ -182,8 +181,7 @@ 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,10 +1,6 @@
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;
@ -13,55 +9,6 @@ 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

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

View File

@ -1,53 +0,0 @@
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

@ -0,0 +1,49 @@
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

@ -18,7 +18,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId> <PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.11.0</VersionPrefix> <VersionPrefix>10.12.0</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,20 +1,63 @@
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>
/// A class representing a query to the activity logs.
/// </summary>
public class ActivityLogQuery : PaginatedQuery
{ {
/// <summary> /// <summary>
/// A class representing a query to the activity logs. /// Gets or sets a value indicating whether to take entries with a user id.
/// </summary> /// </summary>
public class ActivityLogQuery : PaginatedQuery public bool? HasUserId { get; set; }
{
/// <summary>
/// Gets or sets a value indicating whether to take entries with a user id.
/// </summary>
public bool? HasUserId { get; set; }
/// <summary> /// <summary>
/// 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 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,103 +1,198 @@
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>
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
/// </summary>
public class ActivityManager : IActivityManager
{ {
private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <summary> /// <summary>
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances. /// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// </summary> /// </summary>
public class ActivityManager : IActivityManager /// <param name="provider">The Jellyfin database provider.</param>
public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
{ {
private readonly IDbContextFactory<JellyfinDbContext> _provider; _provider = provider;
}
/// <summary> /// <inheritdoc/>
/// Initializes a new instance of the <see cref="ActivityManager"/> class. public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
/// </summary>
/// <param name="provider">The Jellyfin database provider.</param> /// <inheritdoc/>
public ActivityManager(IDbContextFactory<JellyfinDbContext> provider) public async Task CreateAsync(ActivityLog entry)
{
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{ {
_provider = provider; dbContext.ActivityLogs.Add(entry);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
/// <inheritdoc/> EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated; }
/// <inheritdoc/> /// <inheritdoc/>
public async Task CreateAsync(ActivityLog entry) 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);
await using (dbContext.ConfigureAwait(false))
{ {
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); // TODO switch to LeftJoin in .NET 10.
await using (dbContext.ConfigureAwait(false)) var entries = from a in dbContext.ActivityLogs
join u in dbContext.Users on a.UserId equals u.Id into ugj
from u in ugj.DefaultIfEmpty()
select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
if (query.HasUserId is not null)
{ {
dbContext.ActivityLogs.Add(entry); entries = entries.Where(e => e.ActivityLog.UserId.Equals(default) != query.HasUserId.Value);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); if (query.MinDate is not null)
}
/// <inheritdoc/>
public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
{
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{ {
var entries = dbContext.ActivityLogs entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value);
.OrderByDescending(entry => entry.DateCreated)
.Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate)
.Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value);
return new QueryResult<ActivityLogEntry>(
query.Skip,
await entries.CountAsync().ConfigureAwait(false),
await entries
.Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
.Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId)
{
Id = entity.Id,
Overview = entity.Overview,
ShortOverview = entity.ShortOverview,
ItemId = entity.ItemId,
Date = entity.DateCreated,
Severity = entity.LogSeverity
})
.ToListAsync()
.ConfigureAwait(false));
} }
}
/// <inheritdoc /> if (!string.IsNullOrEmpty(query.Name))
public async Task CleanAsync(DateTime startDate)
{
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{ {
await dbContext.ActivityLogs entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%"));
.Where(entry => entry.DateCreated <= startDate)
.ExecuteDeleteAsync()
.ConfigureAwait(false);
} }
}
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) if (!string.IsNullOrEmpty(query.Overview))
{
return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
{ {
Id = entry.Id, entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Overview, $"%{query.Overview}%"));
Overview = entry.Overview, }
ShortOverview = entry.ShortOverview,
ItemId = entry.ItemId, if (!string.IsNullOrEmpty(query.ShortOverview))
Date = entry.DateCreated, {
Severity = entry.LogSeverity 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>(
query.Skip,
await entries.CountAsync().ConfigureAwait(false),
await ApplyOrdering(entries, query.OrderBy)
.Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
.Select(entity => new ActivityLogEntry(entity.ActivityLog.Name, entity.ActivityLog.Type, entity.ActivityLog.UserId)
{
Id = entity.ActivityLog.Id,
Overview = entity.ActivityLog.Overview,
ShortOverview = entity.ActivityLog.ShortOverview,
ItemId = entity.ActivityLog.ItemId,
Date = entity.ActivityLog.DateCreated,
Severity = entity.ActivityLog.LogSeverity
})
.ToListAsync()
.ConfigureAwait(false));
} }
} }
/// <inheritdoc />
public async Task CleanAsync(DateTime startDate)
{
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
await dbContext.ActivityLogs
.Where(entry => entry.DateCreated <= startDate)
.ExecuteDeleteAsync()
.ConfigureAwait(false);
}
}
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
{
return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
{
Id = entry.Id,
Overview = entry.Overview,
ShortOverview = entry.ShortOverview,
ItemId = entry.ItemId,
Date = entry.DateCreated,
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);
ordered = sortOrder == SortOrder.Ascending
? (ordered ?? query).OrderBy(orderBy)
: (ordered ?? query).OrderByDescending(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) if (query.Limit.HasValue && query.Limit.Value > 0)
{ {
devices = devices.Take(query.Limit.Value); devices = devices.Take(query.Limit.Value);
} }

View File

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

View File

@ -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.HasValue && (filter.StartIndex ?? 0) == 0)) if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
{ {
var returnList = GetItemList(filter); var returnList = GetItemList(filter);
return new QueryResult<BaseItemDto>( return new QueryResult<BaseItemDto>(
@ -275,6 +275,7 @@ public sealed class BaseItemRepository
} }
dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
result.StartIndex = filter.StartIndex ?? 0; result.StartIndex = filter.StartIndex ?? 0;
@ -294,6 +295,7 @@ public sealed class BaseItemRepository
dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
} }
@ -324,7 +326,7 @@ public sealed class BaseItemRepository
.OrderByDescending(g => g.MaxDateCreated) .OrderByDescending(g => g.MaxDateCreated)
.Select(g => g); .Select(g => g);
if (filter.Limit.HasValue) if (filter.Limit.HasValue && filter.Limit.Value > 0)
{ {
subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value); subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
} }
@ -337,6 +339,8 @@ public sealed class BaseItemRepository
mainquery = ApplyGroupingFilter(context, mainquery, filter); mainquery = ApplyGroupingFilter(context, mainquery, filter);
mainquery = ApplyQueryPaging(mainquery, filter); mainquery = ApplyQueryPaging(mainquery, filter);
mainquery = ApplyNavigations(mainquery, filter);
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
} }
@ -363,7 +367,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) if (filter.Limit.HasValue && filter.Limit.Value > 0)
{ {
query = query.Take(filter.Limit.Value); query = query.Take(filter.Limit.Value);
} }
@ -399,9 +403,7 @@ public sealed class BaseItemRepository
dbQuery = dbQuery.Distinct(); dbQuery = dbQuery.Distinct();
} }
dbQuery = ApplyOrder(dbQuery, filter); dbQuery = ApplyOrder(dbQuery, filter, context);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery; return dbQuery;
} }
@ -423,19 +425,14 @@ public sealed class BaseItemRepository
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{ {
if (filter.Limit.HasValue || filter.StartIndex.HasValue) if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
{ {
var offset = filter.StartIndex ?? 0; dbQuery = dbQuery.Skip(filter.StartIndex.Value);
}
if (offset > 0) if (filter.Limit.HasValue && filter.Limit.Value > 0)
{ {
dbQuery = dbQuery.Skip(offset); dbQuery = dbQuery.Take(filter.Limit.Value);
}
if (filter.Limit.HasValue)
{
dbQuery = dbQuery.Take(filter.Limit.Value);
}
} }
return dbQuery; return dbQuery;
@ -446,6 +443,7 @@ public sealed class BaseItemRepository
dbQuery = TranslateQuery(dbQuery, context, filter); dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery; return dbQuery;
} }
@ -549,22 +547,34 @@ public sealed class BaseItemRepository
} }
/// <inheritdoc /> /// <inheritdoc />
public void SaveImages(BaseItemDto item) public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default)
{ {
ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(item);
var images = item.ImageInfos.Select(e => Map(item.Id, e)); var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray();
using var context = _dbProvider.CreateDbContext();
if (!context.BaseItems.Any(bi => bi.Id == item.Id)) var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{ {
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); if (!await context.BaseItems
return; .AnyAsync(bi => bi.Id == item.Id, cancellationToken)
} .ConfigureAwait(false))
{
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
return;
}
context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); await context.BaseItemImageInfos
context.BaseItemImageInfos.AddRange(images); .Where(e => e.ItemId == item.Id)
context.SaveChanges(); .ExecuteDeleteAsync(cancellationToken)
.ConfigureAwait(false);
await context.BaseItemImageInfos
.AddRangeAsync(images, cancellationToken)
.ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -614,6 +624,19 @@ public sealed class BaseItemRepository
else else
{ {
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete(); context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
if (entity.Images is { Count: > 0 })
{
context.BaseItemImageInfos.AddRange(entity.Images);
}
if (entity.LockedFields is { Count: > 0 })
{
context.BaseItemMetadataFields.AddRange(entity.LockedFields);
}
context.BaseItems.Attach(entity).State = EntityState.Modified; context.BaseItems.Attach(entity).State = EntityState.Modified;
} }
} }
@ -1174,7 +1197,7 @@ public sealed class BaseItemRepository
{ {
ArgumentNullException.ThrowIfNull(filter); ArgumentNullException.ThrowIfNull(filter);
if (!filter.Limit.HasValue) if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
{ {
filter.EnableTotalRecordCount = false; filter.EnableTotalRecordCount = false;
} }
@ -1245,7 +1268,7 @@ public sealed class BaseItemRepository
.AsSingleQuery() .AsSingleQuery()
.Where(e => masterQuery.Contains(e.Id)); .Where(e => masterQuery.Contains(e.Id));
query = ApplyOrder(query, filter); query = ApplyOrder(query, filter, context);
var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
if (filter.EnableTotalRecordCount) if (filter.EnableTotalRecordCount)
@ -1253,19 +1276,14 @@ public sealed class BaseItemRepository
result.TotalRecordCount = query.Count(); result.TotalRecordCount = query.Count();
} }
if (filter.Limit.HasValue || filter.StartIndex.HasValue) if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
{ {
var offset = filter.StartIndex ?? 0; query = query.Skip(filter.StartIndex.Value);
}
if (offset > 0) if (filter.Limit.HasValue && filter.Limit.Value > 0)
{ {
query = query.Skip(offset); query = query.Take(filter.Limit.Value);
}
if (filter.Limit.HasValue)
{
query = query.Take(filter.Limit.Value);
}
} }
IQueryable<BaseItemEntity>? itemCountQuery = null; IQueryable<BaseItemEntity>? itemCountQuery = null;
@ -1346,7 +1364,7 @@ public sealed class BaseItemRepository
private static void PrepareFilterQuery(InternalItemsQuery query) private static void PrepareFilterQuery(InternalItemsQuery query)
{ {
if (query.Limit.HasValue && query.EnableGroupByMetadataKey) if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
{ {
query.Limit = query.Limit.Value + 4; query.Limit = query.Limit.Value + 4;
} }
@ -1357,14 +1375,54 @@ public sealed class BaseItemRepository
} }
} }
private string GetCleanValue(string value) /// <summary>
/// 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;
} }
return value.RemoveDiacritics().ToLowerInvariant(); var noDiacritics = value.RemoveDiacritics();
// 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)
@ -1511,7 +1569,7 @@ public sealed class BaseItemRepository
|| query.IncludeItemTypes.Contains(BaseItemKind.Season); || query.IncludeItemTypes.Contains(BaseItemKind.Season);
} }
private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter) private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context)
{ {
var orderBy = filter.OrderBy; var orderBy = filter.OrderBy;
var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
@ -1530,7 +1588,7 @@ public sealed class BaseItemRepository
var firstOrdering = orderBy.FirstOrDefault(); var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default) if (firstOrdering != default)
{ {
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter); var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
if (firstOrdering.SortOrder == SortOrder.Ascending) if (firstOrdering.SortOrder == SortOrder.Ascending)
{ {
orderedQuery = query.OrderBy(expression); orderedQuery = query.OrderBy(expression);
@ -1555,7 +1613,7 @@ public sealed class BaseItemRepository
foreach (var item in orderBy.Skip(1)) foreach (var item in orderBy.Skip(1))
{ {
var expression = OrderMapper.MapOrderByField(item.OrderBy, filter); var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
if (item.SortOrder == SortOrder.Ascending) if (item.SortOrder == SortOrder.Ascending)
{ {
orderedQuery = orderedQuery!.ThenBy(expression); orderedQuery = orderedQuery!.ThenBy(expression);
@ -1637,19 +1695,18 @@ public sealed class BaseItemRepository
var tags = filter.Tags.ToList(); var tags = filter.Tags.ToList();
var excludeTags = filter.ExcludeTags.ToList(); var excludeTags = filter.ExcludeTags.ToList();
if (filter.IsMovie == true) if (filter.IsMovie.HasValue)
{ {
if (filter.IncludeItemTypes.Length == 0 var shouldIncludeAllMovieTypes = filter.IsMovie.Value
|| filter.IncludeItemTypes.Contains(BaseItemKind.Movie) && (filter.IncludeItemTypes.Length == 0
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
if (!shouldIncludeAllMovieTypes)
{ {
baseQuery = baseQuery.Where(e => e.IsMovie); baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
} }
} }
else if (filter.IsMovie.HasValue)
{
baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
}
if (filter.IsSeries.HasValue) if (filter.IsSeries.HasValue)
{ {
@ -1694,15 +1751,16 @@ public sealed class BaseItemRepository
if (!string.IsNullOrEmpty(filter.SearchTerm)) if (!string.IsNullOrEmpty(filter.SearchTerm))
{ {
var searchTerm = filter.SearchTerm.ToLower(); var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
if (SearchWildcardTerms.Any(f => searchTerm.Contains(f))) var originalSearchTerm = filter.SearchTerm.ToLower();
if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
{ {
searchTerm = $"%{searchTerm.Trim('%')}%"; cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!.ToLower(), searchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), searchTerm))); baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), originalSearchTerm)));
} }
else else
{ {
baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm))); baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm)));
} }
} }
@ -1756,7 +1814,8 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.Path)) if (!string.IsNullOrWhiteSpace(filter.Path))
{ {
baseQuery = baseQuery.Where(e => e.Path == filter.Path); var pathToQuery = GetPathToSave(filter.Path);
baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
} }
if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
@ -1936,19 +1995,20 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
{ {
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith)); var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
} }
if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
{ {
// i hate this var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]); baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
} }
if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
{ {
// i hate this var lessThanLower = filter.NameLessThan.ToLowerInvariant();
baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]); baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
} }
if (filter.ImageTypes.Length > 0) if (filter.ImageTypes.Length > 0)
@ -2046,7 +2106,7 @@ public sealed class BaseItemRepository
if (filter.ExcludeArtistIds.Length > 0) if (filter.ExcludeArtistIds.Length > 0)
{ {
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true); baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
} }
if (filter.GenreIds.Count > 0) if (filter.GenreIds.Count > 0)
@ -2353,17 +2413,23 @@ public sealed class BaseItemRepository
if (filter.HasImdbId.HasValue) if (filter.HasImdbId.HasValue)
{ {
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); baseQuery = filter.HasImdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
} }
if (filter.HasTmdbId.HasValue) if (filter.HasTmdbId.HasValue)
{ {
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); baseQuery = filter.HasTmdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
} }
if (filter.HasTvdbId.HasValue) if (filter.HasTvdbId.HasValue)
{ {
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); baseQuery = filter.HasTvdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
} }
var queryTopParentIds = filter.TopParentIds; var queryTopParentIds = filter.TopParentIds;
@ -2401,39 +2467,34 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0) if (filter.ExcludeInheritedTags.Length > 0)
{ {
baseQuery = baseQuery baseQuery = baseQuery.Where(e =>
.Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags) !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
.Any(f => filter.ExcludeInheritedTags.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)
{ {
// Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. // For seasons and episodes, we also need to check the parent series' tags.
// In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
{ {
baseQuery = baseQuery baseQuery = baseQuery.Where(e =>
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
.Any(f => 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))));
||
(e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags))
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
} }
// A playlist should be accessible to its owner regardless of allowed tags. // A playlist should be accessible to its owner regardless of allowed tags.
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
{ {
baseQuery = baseQuery baseQuery = baseQuery.Where(e =>
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
// d ^^ this is stupid it hate this. // d ^^ this is stupid it hate this.
} }
else else
{ {
baseQuery = baseQuery baseQuery = baseQuery.Where(e =>
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
} }
} }

View File

@ -1,7 +1,10 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System; using System;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -18,39 +21,50 @@ public static class OrderMapper
/// </summary> /// </summary>
/// <param name="sortBy">Item property to sort by.</param> /// <param name="sortBy">Item property to sort by.</param>
/// <param name="query">Context Query.</param> /// <param name="query">Context Query.</param>
/// <param name="jellyfinDbContext">Context.</param>
/// <returns>Func to be executed later for sorting query.</returns> /// <returns>Func to be executed later for sorting query.</returns>
public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query, JellyfinDbContext jellyfinDbContext)
{ {
return sortBy switch return (sortBy, query.User) switch
{ {
ItemSortBy.AirTime => e => e.SortName, // TODO (ItemSortBy.AirTime, _) => e => e.SortName, // TODO
ItemSortBy.Runtime => e => e.RunTimeTicks, (ItemSortBy.Runtime, _) => e => e.RunTimeTicks,
ItemSortBy.Random => e => EF.Functions.Random(), (ItemSortBy.Random, _) => e => EF.Functions.Random(),
ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate, (ItemSortBy.DatePlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate,
ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount, (ItemSortBy.PlayCount, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount,
ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite, (ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite,
ItemSortBy.IsFolder => e => e.IsFolder, (ItemSortBy.IsFolder, _) => e => e.IsFolder,
ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, (ItemSortBy.IsPlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, (ItemSortBy.IsUnplayed, _) => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, (ItemSortBy.DateLastContentAdded, _) => e => e.DateLastMediaAdded,
ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), (ItemSortBy.Artist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), (ItemSortBy.AlbumArtist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), (ItemSortBy.Studio, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, (ItemSortBy.OfficialRating, _) => e => e.InheritedParentalRatingValue,
// ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", (ItemSortBy.SeriesSortName, _) => e => e.SeriesName,
ItemSortBy.SeriesSortName => e => e.SeriesName, (ItemSortBy.Album, _) => e => e.Album,
(ItemSortBy.DateCreated, _) => e => e.DateCreated,
(ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
(ItemSortBy.StartDate, _) => e => e.StartDate,
(ItemSortBy.Name, _) => e => e.CleanName,
(ItemSortBy.CommunityRating, _) => e => e.CommunityRating,
(ItemSortBy.ProductionYear, _) => e => e.ProductionYear,
(ItemSortBy.CriticRating, _) => e => e.CriticRating,
(ItemSortBy.VideoBitRate, _) => e => e.TotalBitrate,
(ItemSortBy.ParentIndexNumber, _) => e => e.ParentIndexNumber,
(ItemSortBy.IndexNumber, _) => e => e.IndexNumber,
(ItemSortBy.SeriesDatePlayed, not null) => e =>
jellyfinDbContext.BaseItems
.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
.Join(jellyfinDbContext.UserData.Where(w => w.UserId == query.User.Id && w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
.Max(f => f),
(ItemSortBy.SeriesDatePlayed, null) => e => jellyfinDbContext.BaseItems.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
.Join(jellyfinDbContext.UserData.Where(w => w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
.Max(f => f),
// ItemSortBy.SeriesDatePlayed => e => jellyfinDbContext.UserData
// .Where(u => u.Item!.SeriesPresentationUniqueKey == e.PresentationUniqueKey && u.Played)
// .Max(f => f.LastPlayedDate),
// ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
ItemSortBy.Album => e => e.Album,
ItemSortBy.DateCreated => e => e.DateCreated,
ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
ItemSortBy.StartDate => e => e.StartDate,
ItemSortBy.Name => e => e.CleanName,
ItemSortBy.CommunityRating => e => e.CommunityRating,
ItemSortBy.ProductionYear => e => e.ProductionYear,
ItemSortBy.CriticRating => e => e.CriticRating,
ItemSortBy.VideoBitRate => e => e.TotalBitrate,
ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
ItemSortBy.IndexNumber => e => e.IndexNumber,
_ => e => e.SortName _ => e => e.SortName
}; };
} }

View File

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

View File

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

View File

@ -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 (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(resolvedUser.Password) && string.IsNullOrEmpty(password))
{ {
return Task.FromResult(new ProviderAuthenticationResult return Task.FromResult(new ProviderAuthenticationResult
{ {
@ -93,10 +93,6 @@ 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

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

View File

@ -21,12 +21,6 @@ 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)
{ {

View File

@ -306,15 +306,12 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public UserDto GetUserDto(User user, string? remoteEndPoint = null) public UserDto GetUserDto(User user, string? remoteEndPoint = null)
{ {
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications; var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
return new UserDto return new UserDto
{ {
Name = user.Username, Name = user.Username,
Id = user.Id, Id = user.Id,
ServerId = _appHost.SystemId, ServerId = _appHost.SystemId,
HasPassword = hasPassword,
HasConfiguredPassword = hasPassword,
EnableAutoLogin = user.EnableAutoLogin, EnableAutoLogin = user.EnableAutoLogin,
LastLoginDate = user.LastLoginDate, LastLoginDate = user.LastLoginDate,
LastActivityDate = user.LastActivityDate, LastActivityDate = user.LastActivityDate,
@ -508,23 +505,18 @@ namespace Jellyfin.Server.Implementations.Users
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{ {
var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
var passwordResetProvider = GetPasswordResetProvider(user);
var result = await passwordResetProvider
.StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
.ConfigureAwait(false);
if (user is not null && isInNetwork) if (user is not null && isInNetwork)
{ {
var passwordResetProvider = GetPasswordResetProvider(user);
var result = await passwordResetProvider
.StartForgotPasswordProcess(user, isInNetwork)
.ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false);
return result;
} }
return new ForgotPasswordResult return result;
{
Action = ForgotPasswordAction.InNetworkRequired,
PinFile = string.Empty
};
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -760,8 +752,13 @@ namespace Jellyfin.Server.Implementations.Users
return GetAuthenticationProviders(user)[0]; return GetAuthenticationProviders(user)[0];
} }
private IPasswordResetProvider GetPasswordResetProvider(User user) private IPasswordResetProvider GetPasswordResetProvider(User? user)
{ {
if (user is null)
{
return _defaultPasswordResetProvider;
}
return GetPasswordResetProviders(user)[0]; return GetPasswordResetProviders(user)[0];
} }

View File

@ -117,18 +117,5 @@ namespace Jellyfin.Server.Extensions
{ {
return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>(); return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>();
} }
/// <summary>
/// Adds /emby and /mediabrowser route trimming to the application pipeline.
/// </summary>
/// <remarks>
/// This must be injected before any path related middleware.
/// </remarks>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>();
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to disable legacy authorization in the system config.
/// </summary>
[JellyfinMigration("2025-11-18T16:00:00", nameof(DisableLegacyAuthorization))]
public class DisableLegacyAuthorization : IAsyncMigrationRoutine
{
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="DisableLegacyAuthorization"/> class.
/// </summary>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public DisableLegacyAuthorization(IServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc />
public Task PerformAsync(CancellationToken cancellationToken)
{
_serverConfigurationManager.Configuration.EnableLegacyAuthorization = false;
_serverConfigurationManager.SaveConfiguration();
return Task.CompletedTask;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,105 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.ServerSetupApp;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to refresh CleanName values for all library items.
/// </summary>
[JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))]
[JellyfinMigrationBackup(JellyfinDb = true)]
public class RefreshCleanNames : IAsyncMigrationRoutine
{
private readonly IStartupLogger<RefreshCleanNames> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshCleanNames"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public RefreshCleanNames(
IStartupLogger<RefreshCleanNames> logger,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_logger = logger;
_dbProvider = dbProvider;
}
/// <inheritdoc />
public async Task PerformAsync(CancellationToken cancellationToken)
{
const int Limit = 1000;
int itemCount = 0;
var sw = Stopwatch.StartNew();
using var context = _dbProvider.CreateDbContext();
var records = context.BaseItems.Count(b => !string.IsNullOrEmpty(b.Name));
_logger.LogInformation("Refreshing CleanName for {Count} library items", records);
var processedInPartition = 0;
await foreach (var item in context.BaseItems
.Where(b => !string.IsNullOrEmpty(b.Name))
.OrderBy(e => e.Id)
.WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed))
.PartitionEagerAsync(Limit, cancellationToken)
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
try
{
var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : BaseItemRepository.GetCleanValue(item.Name);
if (!string.Equals(newCleanName, item.CleanName, StringComparison.Ordinal))
{
_logger.LogDebug(
"Updating CleanName for item {Id}: '{OldValue}' -> '{NewValue}'",
item.Id,
item.CleanName,
newCleanName);
item.CleanName = newCleanName;
itemCount++;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update CleanName for item {Id} ({Name})", item.Id, item.Name);
}
processedInPartition++;
if (processedInPartition >= Limit)
{
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
// Clear tracked entities to avoid memory growth across partitions
context.ChangeTracker.Clear();
processedInPartition = 0;
}
}
// Save any remaining changes after the loop
if (processedInPartition > 0)
{
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
context.ChangeTracker.Clear();
}
_logger.LogInformation(
"Refreshed CleanName for {UpdatedCount} out of {TotalCount} items in {Time}",
itemCount,
records,
sw.Elapsed);
}
}

View File

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

View File

@ -11,7 +11,7 @@
{ {
"Name": "Console", "Name": "Console",
"Args": { "Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}" "outputTemplate": "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
} }
}, },
{ {

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