Compare commits

...

116 Commits

Author SHA1 Message Date
daswesen123
e6cd73df03 Translated using Weblate (English (Pirate))
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en@pirate/
2025-09-27 22:14:14 +00:00
Corentin Malbet
71ebb1f456 Fixing the UFID field value giving a warning and not being correctly processed (#14851)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2025-09-26 14:24:59 -06:00
Tim Eisele
9c298c52f5 Expose ExtractAllExtractableSubtitles (#14876) 2025-09-26 13:45:01 -06:00
Niels van Velzen
3e8db40901 Merge pull request #14874 from jellyfin/renovate/polly-monorepo
Update dependency Polly to 8.6.4
2025-09-26 21:39:47 +02:00
Niels van Velzen
f9ead9615c Merge pull request #14855 from jellyfin/renovate/ci-deps
Update CI dependencies
2025-09-26 21:39:16 +02:00
Niels van Velzen
93af2d6f67 Merge pull request #14873 from theguymadmax/use-listorder
Restore NFO/import ordering by using ListOrder instead of SortOrder
2025-09-26 21:38:29 +02:00
renovate[bot]
027c91949d Update CI dependencies 2025-09-26 17:50:59 +00:00
JPVenson
526ec83305 Add Jellyfin.CodeAnalysis project to abi diff (#14875) 2025-09-26 11:49:51 -06:00
renovate[bot]
dfcacce1b0 Update dependency Polly to 8.6.4 2025-09-26 15:13:09 +00:00
theguymadmax
2a54669a8a Restore NFO/import ordering by using ListOrder instead of SortOrder 2025-09-26 10:49:38 -04:00
JPVenson
54d48fa446 Fix people deduplication lookup (#14864)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2025-09-25 19:27:38 -06:00
JPVenson
1736a566cc Fixes FK on unconnected base items (#14863) 2025-09-25 19:27:17 -06:00
gnattu
04ab362e59 Revert "Update skiasharp monorepo (#14849)" (#14862)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-25 16:05:04 +02:00
JPVenson
e282b05b8f fixes #14859 Add Check for ItemValues (#14860) 2025-09-25 08:02:20 -06:00
JPVenson
2aa39226c6 Apply filter server side (#14856)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2025-09-24 18:15:10 -06:00
theguymadmax
60fbd39bb9 Fix people sort order (#14852) 2025-09-24 17:37:07 -06:00
JPVenson
740b9924a0 Include ListOrder on Import (#14854) 2025-09-24 15:22:05 -06:00
JPVenson
5a6d9180fe Add People Dedup and multiple progress fixes (#14848) 2025-09-24 15:20:30 -06:00
theguymadmax
897975fc57 Fix collections one-off (#14814) 2025-09-24 15:19:15 -06:00
Bond-009
7dab62616f Merge pull request #14827 from tjwalkr3/warnings-3
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Fix CA1051 warning, Change public field to auto-property
2025-09-24 19:32:24 +02:00
renovate[bot]
f1bd9a40d5 Update skiasharp monorepo (#14849)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 08:28:46 +02:00
renovate[bot]
469e6e1bc8 Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.15 (#14830)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-23 19:52:31 -06:00
JPVenson
38f5f8008a Fix ordering where exists (#14843) 2025-09-23 19:51:44 -06:00
JPVenson
7bb68d8610 Fix Image loading (#14842)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-23 07:02:30 -06:00
Cody Robibero
27047c35a4 Add schema to 503 headers (#14840) 2025-09-23 07:00:34 -06:00
Looooke
42003ca9d2 Translated using Weblate (Alemannic)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/
2025-09-22 22:16:10 +00:00
JPVenson
98f5e21bb8 Fix groupings not applied (#14826) 2025-09-22 15:31:21 -06:00
Mikal S.
162985bb23 fix: add back missing behavior for HasAnyProviderId (#14831)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-22 09:56:41 -06:00
Jan Zachar
0d2c551cce Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/
2025-09-22 12:04:53 +00:00
Janniry Belen
717e7cbd77 Translated using Weblate (Spanish (Dominican Republic))
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_DO/
2025-09-22 04:08:11 +00:00
Thomas Jones
58f9bdcf5c Added ourselves to CONTRIBUTORS.md
Co-authored-by: Derpipose <90276123+Derpipose@users.noreply.github.com>
2025-09-20 23:58:49 -06:00
Thomas Jones
2a499aaa95 Fix CA1051 warnings in EncodingJobInfo.cs
Convert public fields to auto-properties and fix member ordering

Co-authored-by: Derpipose <90276123+Derpipose@users.noreply.github.com>
2025-09-20 23:31:58 -06:00
evan314159
4246825239 Attach before updating/deleting to avoid DbUpdateConcurrencyException (#14746)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2025-09-20 07:23:04 -06:00
renovate[bot]
68810c690b Update dependency z440.atl.core to 7.5.0 (#14793) 2025-09-20 07:22:10 -06:00
Tim Eisele
b73ea1b99d Skip removed images (#14823) 2025-09-20 07:20:21 -06:00
JPVenson
59f77c24c9 Revert limit hack (#14820) 2025-09-20 07:19:26 -06:00
JPVenson
0949212993 Make migration handle parent cleanup (#14817)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
* Make migration handle parent cleanup

* Remove speed improvement

* Update MigrateLibraryDb.cs
2025-09-19 13:17:31 -06:00
renovate[bot]
248aac9a3a Update dependency Svg.Skia to 3.2.1 (#14815)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 11:48:16 -06:00
JPVenson
a1b85a63e7 Fix root folder not being saved to Db if nessesary (#14819)
* Fix root folder not being saved to Db if nessesary

* Always update folder to Db
2025-09-19 11:47:41 -06:00
Kendall Garner
091cb1c34a Fix playlist move from smaller to larger index (#14794)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-18 08:37:31 -06:00
JPVenson
eaf33f01e1 #14751 Only migrate providerids that match assumption (#14810)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-17 18:33:23 -06:00
renovate[bot]
db2dbaa62b Update Microsoft to 4.14.0 (#14808)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:08:28 -06:00
renovate[bot]
1a7df6daf7 Update dependency Newtonsoft.Json to 13.0.4 (#14807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:08:24 -06:00
JPVenson
a0b3e2b071 Optimize internal querying of UserData, other fixes (#14795) 2025-09-16 13:08:04 -06:00
evan314159
2618a5fba2 Fix sync disposal of async-created IAsyncDisposable objects (#14755)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2025-09-16 11:14:52 +02:00
Bond-009
2ee887a502 Merge pull request #14800 from jellyfin/renovate/tmdblib-2.x
Update dependency TMDbLib to 2.3.0
2025-09-16 11:13:01 +02:00
Bond-009
a17e157d44 Merge pull request #14799 from Shadowghost/add-ec3
Add ec3 to audio file extensions
2025-09-16 11:12:41 +02:00
renovate[bot]
6b6745b7fe Update dependency TMDbLib to 2.3.0 2025-09-15 02:51:21 +00:00
Shadowghost
594f9e4f6b Add ec3 to audio file extensions 2025-09-14 23:23:04 +02:00
Bond-009
4cda5f5ff2 Merge pull request #14790 from jellyfin/renovate/ci-deps
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.13
2025-09-14 21:39:20 +02:00
JPVenson
24410d8a2e Reenable common PRAGMA setters (#14791) 2025-09-14 11:24:35 -06:00
Cody Robibero
4d36bd635d Revert IsPlayed optimization, pass UserItemData to IsPlayed when available (#14786) 2025-09-14 11:18:21 -06:00
renovate[bot]
ef65534071 Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.13 2025-09-12 23:56:20 +00:00
KGT1
7c6cedd90a Allow non-admin users to subscribe to their own Sessions (#13767)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2025-09-12 14:15:00 -06:00
theguymadmax
96590eea85 Fix duplicate media entries (#14404) 2025-09-12 13:58:42 -06:00
Bond-009
6796b3435d Avoid constant arrays as arguments (#14784) 2025-09-12 13:58:28 -06:00
Bond-009
8776a447d1 Various cleanups (#14785) 2025-09-12 13:58:23 -06:00
JPVenson
c02a24e32a Fix several Stackoverflows (#14783) 2025-09-12 13:58:16 -06:00
Bond-009
deee04ae38 Add fast path to check for empty ignore files (#14782) 2025-09-12 13:58:02 -06:00
Tim Eisele
580db0c1d2 Never replace BoxSet LinkedChildren on update (#14723) 2025-09-12 13:57:55 -06:00
Alex Collado
8fcc2496d9 Change Spanish order in iso6392.txt to favor Castillian (#14777) 2025-09-12 13:57:48 -06:00
JPVenson
f0e60a7ff3 Improve optimistic locking behavior (#14779) 2025-09-12 13:57:40 -06:00
JPVenson
a99e67544a Reenable pooling (#14778) 2025-09-12 13:57:33 -06:00
nenadsuperzmaj
bca6400bc3 Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sr/
2025-09-12 18:09:53 +00:00
theguymadmax
986a509955 Add 1-second tolerance to resume playback completion check (#14774)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-11 15:24:23 -06:00
theguymadmax
da19f02f7b Sort trailers before teasers (#14715) 2025-09-11 15:23:41 -06:00
Bond-009
3fad5eb069 Make private Emby.Naming.Video.StackResolver.StackMetadata sealed to silence compiler warning (#14764)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
2025-09-11 10:18:47 +02:00
renovate[bot]
9923a51aed Update dependency dotnet-ef to v9.0.9 (#14768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 10:18:20 +02:00
renovate[bot]
585e9a2fe2 Update Microsoft to 9.0.9 (#14769)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 10:18:06 +02:00
renovate[bot]
8e81737dba Update github/codeql-action action to v3.30.3 (#14767)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 10:09:59 +02:00
Tim Eisele
e4e578b37a Don't use ffprobe frame options on audio probe (#14773)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2025-09-10 20:32:14 -06:00
Looooke
387bc0c8eb Translated using Weblate (Alemannic)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/
2025-09-09 23:20:42 +00:00
Varoon Pazhyanur
cbb569a277 Make private Emby.Naming.Video.StackResolver.StackMetadata sealed to silence compiler warning 2025-09-08 21:21:43 -04:00
Adrián HM
1fa63b797b Translated using Weblate (Spanish (Mexico))
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_MX/
2025-09-08 22:32:30 +00:00
Magnus Antonsen
aa3a7c88a4 Translated using Weblate (Norwegian Bokmål)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nb_NO/
2025-09-08 15:02:04 +00:00
evan314159
0a2cf69a55 Additional debug logging for SQLite connections (#14753)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2025-09-07 14:40:27 -06:00
theguymadmax
0845b0c258 Skip non-media folders in movie resolver (#14724)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
* Skip non-media folders in movie resolver

* Ignorepatterns first
2025-09-07 13:02:52 +02:00
theguymadmax
e043f93a72 Preserve 3D format on metadata refresh (#14742)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-06 11:38:00 -06:00
Arty
6ac2d707cb Translated using Weblate (Russian)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/
2025-09-06 04:51:32 +00:00
JPVenson
20f7ddbf8f Refactor Display preference manager (#14056)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
2025-09-05 14:39:15 -06:00
renovate[bot]
4849486fa0 Update github/codeql-action action to v3.30.1 (#14748)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 17:13:46 +02:00
renovate[bot]
4ccd3da77a Update actions/stale action to v10 (#14741)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 11:10:59 +02:00
renovate[bot]
bc28dc11c0 Update actions/setup-python action to v6 (#14740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 11:10:36 +02:00
theguymadmax
d9eaeed61d Fix latest items grouping by collection type (#14736)
* Fix latest items grouping by collection type

* Update Emby.Server.Implementations/Library/UserViewManager.cs

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

---------

Co-authored-by: Bond-009 <bond.009@outlook.com>
2025-09-05 11:05:37 +02:00
Bond-009
c7320dc189 Add more robust error handling for AudioNormalizationTask (#14728)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-03 21:12:24 -06:00
Shane Powell
71048917dd AudioNormalizationTask db progress saving (#14550) 2025-09-03 21:11:58 -06:00
renovate[bot]
11eab1b663 Update actions/setup-dotnet action to v5 (#14738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 21:10:21 -06:00
Md Ashikur Rahman
a17a0495d8 Translated using Weblate (Bengali)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bn/
2025-09-03 20:51:22 +00:00
renovate[bot]
b3e57a5f7d Update github/codeql-action action to v3.30.0 (#14730)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 18:06:23 -06:00
renovate[bot]
65827cce6f Update dependency NEbml to 1.1.0.5 (#14732)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 10:05:05 +02:00
Milo Ivir
b5df0d2a34 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/
2025-09-02 06:43:07 +00:00
ShalokShalom
339a31f0a5 Update .Net Core to .Net (#14718)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-09-01 11:45:24 -06:00
evan314159
a0d4ae1974 Correct Album Artists merge logic (#14655)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
* Correct Album Artists merge logic and Artist equality checks

Correct Album Artists merge logic in MetadataService that causes empty
metadata sources to overwrite populated Album Artists arrays. This impacted
People-to-BaseItem relationships and caused orphaned records in Peoples.

Correct equality checks to be case-sensitive so Jelly metadata exactly
matches file metadata.

* use StringComparer.Ordinal

---------

Co-authored-by: Evan <evan@MacBook-Pro.local>
2025-09-01 13:22:55 +02:00
renovate[bot]
d65b18a7f3 Update dependency Polly to 8.6.3 (#14690)
Some checks failed
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 17:40:51 -06:00
renovate[bot]
cc93b44947 Update dependency Svg.Skia to 3.0.6 (#14691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 17:40:36 -06:00
evan314159
e753adac2c fix ProbeProvider.HasChanged: if file date changed (#14674) 2025-08-27 17:34:51 -06:00
Marc Brooks
0b465842c8 Normalizer cleanup (#14711) 2025-08-27 17:34:00 -06:00
Cody Robibero
da3f3b09d9 Use existing userData (#14703)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2025-08-26 16:09:17 -06:00
Bond-009
7a9beb3745 Merge pull request #14701 from jellyfin/renovate/fscheck.xunit-3.x
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Update dependency FsCheck.Xunit to 3.3.1
2025-08-24 21:26:46 +02:00
renovate[bot]
c7ee07b14a Update dependency FsCheck.Xunit to 3.3.1 2025-08-24 18:51:40 +00:00
Lucas
d8dfbc26f6 Translated using Weblate (Spanish (Argentina))
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/
2025-08-23 16:48:55 +00:00
Shiva Prasad
88e0d35ed7 Translated using Weblate (Telugu)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/te/
2025-08-23 09:57:19 +00:00
evan314159
1eadb07a12 Fix GetSimilarItems to exclude the searched for item Id (#14686)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2025-08-22 19:00:29 -06:00
Gjelbrim Haskaj
26d9633fed Translated using Weblate (Albanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sq/
2025-08-23 00:15:35 +00:00
spurdl
19aadd934b Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/
2025-08-22 20:57:15 +00:00
Bond-009
ce28374d40 Run background ffmpeg tasks as ProcessPriorityClass.BelowNormal (#14651)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Follow TrickPlay example of running other background ffmpeg tasks as ProcessPriorityClass.BelowNormal:

- Keyframe extraction
- Media info probing during library scans
- Audio normalization
- Image extraction

Co-authored-by: Evan <evan@MacBook-Pro.local>
2025-08-22 10:08:29 +02:00
evan314159
7aa1c46447 Merge pull request #14653 from evan314159/coremigration
Delay initialization of singleton services during migration CoreInitialisation stage
2025-08-22 10:06:39 +02:00
Bond-009
ffb7753f8d Merge pull request #14684 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v3.29.11
2025-08-22 10:04:41 +02:00
renovate[bot]
14884f2628 Update github/codeql-action action to v3.29.11 2025-08-21 20:57:46 +00:00
intelligentdonut
41188ff054 Translated using Weblate (English (Pirate))
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en@pirate/
2025-08-19 17:42:50 +00:00
Bond-009
cb6e38d830 Merge pull request #14670 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v3.29.10
2025-08-19 17:16:37 +02:00
renovate[bot]
4ba34709d6 Update github/codeql-action action to v3.29.10 2025-08-18 16:34:37 +00:00
Gene
28b8d3ee29 fix: correct anamorphic video detection (#14640) (#14648)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
2025-08-15 18:52:43 -06:00
MrPlow
9eaca73888 Translated using Weblate (German)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2025-08-15 16:24:00 +00:00
Evan
29e17b6bc0 Run background ffmpeg tasks as ProcessPriorityClass.BelowNormal
Follow TrickPlay example of running other background ffmpeg tasks as ProcessPriorityClass.BelowNormal:

- Keyframe extraction
- Media info probing during library scans
- Audio normalization
- Image extraction
2025-08-15 07:18:44 +08:00
Yago Raña Gayoso
84cde7383f Translated using Weblate (Galician)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gl/
2025-08-14 18:32:52 +00:00
116 changed files with 5452 additions and 857 deletions

View File

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

View File

@@ -294,6 +294,9 @@ dotnet_diagnostic.CA1854.severity = error
# error on CA1860: Avoid using 'Enumerable.Any()' extension method
dotnet_diagnostic.CA1860.severity = error
# error on CA1861: Avoid constant arrays as arguments
dotnet_diagnostic.CA1861.severity = error
# error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
dotnet_diagnostic.CA1862.severity = error

View File

@@ -22,16 +22,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5

View File

@@ -17,7 +17,7 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: '9.0.x'
@@ -47,7 +47,7 @@ jobs:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: '9.0.x'
@@ -105,7 +105,7 @@ jobs:
run: |
{
echo 'body<<EOF'
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll Jellyfin.CodeAnalysis.dll; do
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
printf "\n${file}\n${COMPAT_OUTPUT}\n"

View File

@@ -21,7 +21,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: '9.0.x'
- name: Generate openapi.json
@@ -55,7 +55,7 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: '9.0.x'
- name: Generate openapi.json

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
- uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@c4c5175a441c6603ec614f5084386dabe0e2295b # v5.4.12
uses: danielpalme/ReportGenerator-GitHub-Action@1978db745da4a573ca4baa2d0f67175df51a148c # v5.4.16
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"

View File

@@ -44,7 +44,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.13'
cache: 'pip'

View File

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

View File

@@ -14,7 +14,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.13'
cache: 'pip'

View File

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

View File

@@ -31,6 +31,7 @@
- [DaveChild](https://github.com/DaveChild)
- [DavidFair](https://github.com/DavidFair)
- [Delgan](https://github.com/Delgan)
- [Derpipose](https://github.com/Derpipose)
- [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung)
- [dinki](https://github.com/dinki)
@@ -140,6 +141,7 @@
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
- [thornbill](https://github.com/thornbill)
- [ThreeFive-O](https://github.com/ThreeFive-O)
- [tjwalkr3](https://github.com/tjwalkr3)
- [TrisMcC](https://github.com/TrisMcC)
- [trumblejoe](https://github.com/trumblejoe)
- [TtheCreator](https://github.com/TtheCreator)
@@ -202,6 +204,7 @@
- [Shoham Peller](https://github.com/spellr)
- [theshoeshiner](https://github.com/theshoeshiner)
- [TokerX](https://github.com/TokerX)
- [GeneMarks](https://github.com/GeneMarks)
# Emby Contributors

View File

@@ -19,4 +19,9 @@
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
</ItemGroup>
<!-- Custom Analyzers -->
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' ">
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
</ItemGroup>
</Project>

View File

@@ -17,7 +17,7 @@
<PackageVersion Include="Diacritics" Version="4.0.17" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.1" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
@@ -26,40 +26,43 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.9" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" 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.Data.Sqlite" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.0.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NEbml" Version="1.1.0.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Polly" Version="8.6.2" />
<PackageVersion Include="Polly" Version="8.6.4" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
@@ -70,27 +73,27 @@
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.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.HarfBuzz" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.0.5" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.8" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.8" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.9" />
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.9" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.3.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="z440.atl.core" Version="7.5.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
</Project>
</Project>

View File

@@ -21,8 +21,8 @@ namespace Emby.Naming.Common
/// </summary>
public NamingOptions()
{
VideoFileExtensions = new[]
{
VideoFileExtensions =
[
".001",
".3g2",
".3gp",
@@ -77,10 +77,10 @@ namespace Emby.Naming.Common
".wmv",
".wtv",
".xvid"
};
];
VideoFlagDelimiters = new[]
{
VideoFlagDelimiters =
[
'(',
')',
'-',
@@ -88,15 +88,15 @@ namespace Emby.Naming.Common
'_',
'[',
']'
};
];
StubFileExtensions = new[]
{
StubFileExtensions =
[
".disc"
};
];
StubTypes = new[]
{
StubTypes =
[
new StubTypeRule(
stubType: "dvd",
token: "dvd"),
@@ -136,32 +136,32 @@ namespace Emby.Naming.Common
new StubTypeRule(
stubType: "tv",
token: "DSR")
};
];
VideoFileStackingRules = new[]
{
VideoFileStackingRules =
[
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
};
];
CleanDateTimes = new[]
{
CleanDateTimes =
[
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
};
];
CleanStrings = new[]
{
CleanStrings =
[
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
};
];
SubtitleFileExtensions = new[]
{
SubtitleFileExtensions =
[
".ass",
".mks",
".sami",
@@ -171,17 +171,17 @@ namespace Emby.Naming.Common
".sub",
".sup",
".vtt",
};
];
LyricFileExtensions = new[]
{
LyricFileExtensions =
[
".lrc",
".elrc",
".txt"
};
];
AlbumStackingPrefixes = new[]
{
AlbumStackingPrefixes =
[
"cd",
"digital media",
"disc",
@@ -190,10 +190,10 @@ namespace Emby.Naming.Common
"volume",
"part",
"act"
};
];
ArtistSubfolders = new[]
{
ArtistSubfolders =
[
"albums",
"broadcasts",
"bootlegs",
@@ -208,10 +208,10 @@ namespace Emby.Naming.Common
"soundtracks",
"spokenwords",
"streets"
};
];
AudioFileExtensions = new[]
{
AudioFileExtensions =
[
".669",
".3gp",
".aa",
@@ -241,6 +241,7 @@ namespace Emby.Naming.Common
".dts",
".dvf",
".eac3",
".ec3",
".far",
".flac",
".gdm",
@@ -291,33 +292,33 @@ namespace Emby.Naming.Common
".xm",
".xsp",
".ymf"
};
];
MediaFlagDelimiters = new[]
{
MediaFlagDelimiters =
[
'.'
};
];
MediaForcedFlags = new[]
{
MediaForcedFlags =
[
"foreign",
"forced"
};
];
MediaDefaultFlags = new[]
{
MediaDefaultFlags =
[
"default"
};
];
MediaHearingImpairedFlags = new[]
{
MediaHearingImpairedFlags =
[
"cc",
"hi",
"sdh"
};
];
EpisodeExpressions = new[]
{
EpisodeExpressions =
[
// *** Begin Kodi Standard Naming
// <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 -->
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$")
@@ -330,23 +331,23 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
{
DateTimeFormats =
[
"yyyy.MM.dd",
"yyyy-MM-dd",
"yyyy_MM_dd",
"yyyy MM dd"
}
]
},
new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
{
DateTimeFormats = new[]
{
DateTimeFormats =
[
"dd.MM.yyyy",
"dd-MM-yyyy",
"dd_MM_yyyy",
"dd MM yyyy"
}
]
},
// This isn't a Kodi naming rule, but the expression below causes false episode numbers for
@@ -478,10 +479,10 @@ namespace Emby.Naming.Common
{
IsNamed = true
},
};
];
VideoExtraRules = new[]
{
VideoExtraRules =
[
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.DirectoryName,
@@ -691,14 +692,14 @@ namespace Emby.Naming.Common
ExtraRuleType.Suffix,
"-other",
MediaType.Video)
};
];
AllExtrasTypesFolderNames = VideoExtraRules
.Where(i => i.RuleType == ExtraRuleType.DirectoryName)
.ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
Format3DRules = new[]
{
Format3DRules =
[
// Kodi rules:
new Format3DRule(
precedingToken: "3d",
@@ -725,10 +726,10 @@ namespace Emby.Naming.Common
new Format3DRule("tab"),
new Format3DRule("sbs3d"),
new Format3DRule("mvc")
};
];
AudioBookPartsExpressions = new[]
{
AudioBookPartsExpressions =
[
// Detect specified chapters, like CH 01
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02
@@ -741,14 +742,14 @@ namespace Emby.Naming.Common
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
};
];
AudioBookNamesExpressions = new[]
{
AudioBookNamesExpressions =
[
// Detect year usually in brackets after name Batman (2020)
@"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
@"^\s*(?<name>[^ ].*?)\s*$"
};
];
MultipleEpisodeExpressions = new[]
{
@@ -888,12 +889,12 @@ namespace Emby.Naming.Common
/// <summary>
/// Gets list of clean datetime regular expressions.
/// </summary>
public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] CleanDateTimeRegexes { get; private set; } = [];
/// <summary>
/// Gets list of clean string regular expressions.
/// </summary>
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] CleanStringRegexes { get; private set; } = [];
/// <summary>
/// Compiles raw regex strings into regexes.

View File

@@ -132,7 +132,7 @@ namespace Emby.Naming.Video
}
}
private class StackMetadata
private sealed class StackMetadata
{
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
{

View File

@@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.AppBase
private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
{
var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName);
if (otherMarkers != null)
if (otherMarkers is not null)
{
throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}.");
}

View File

@@ -50,6 +50,8 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
_logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
foreach (var itemId in itemIds)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -95,9 +97,10 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
numComplete++;
double percent = numComplete;
percent /= numItems;
progress.Report(percent * 100);
subProgress.Report(percent * 100);
}
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
@@ -105,7 +108,9 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
await using (transaction.ConfigureAwait(false))
{
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(50);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(100);
}
}

View File

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

View File

@@ -37,6 +37,11 @@ namespace Emby.Server.Implementations.Library
return false;
}
if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
{
return true;
}
// Don't ignore top level folders
if (fileInfo.IsDirectory
&& (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
@@ -44,11 +49,6 @@ namespace Emby.Server.Implementations.Library
return false;
}
if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
{
return true;
}
if (parent is null)
{
return false;

View File

@@ -50,6 +50,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return false;
}
// Fast path in case the ignore files isn't a symlink and is empty
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));

View File

@@ -48,6 +48,8 @@ namespace Emby.Server.Implementations.Library
"**/.wd_tv",
"**/lost+found/**",
"**/lost+found",
"**/subs/**",
"**/subs",
// Trickplay files
"**/*.trickplay",

View File

@@ -327,6 +327,45 @@ namespace Emby.Server.Implementations.Library
DeleteItem(item, options, parent, notifyParentItem);
}
public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items)
{
var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray();
foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
{
foreach (var metadataPath in internalPaths)
{
if (!Directory.Exists(metadataPath))
{
continue;
}
_logger.LogDebug(
"Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
item.Id);
try
{
Directory.Delete(metadataPath, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
}
}
foreach (var fileSystemInfo in pathsToDelete)
{
DeleteItemPath(item, false, fileSystemInfo);
}
}
_itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
}
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
{
ArgumentNullException.ThrowIfNull(item);
@@ -403,59 +442,7 @@ namespace Emby.Server.Implementations.Library
foreach (var fileSystemInfo in item.GetDeletePaths())
{
if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
{
try
{
_logger.LogInformation(
"Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
if (fileSystemInfo.IsDirectory)
{
Directory.Delete(fileSystemInfo.FullName, true);
}
else
{
File.Delete(fileSystemInfo.FullName);
}
}
catch (DirectoryNotFoundException)
{
_logger.LogInformation(
"Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
}
catch (FileNotFoundException)
{
_logger.LogInformation(
"File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
}
catch (IOException)
{
if (isRequiredForDelete)
{
throw;
}
}
catch (UnauthorizedAccessException)
{
if (isRequiredForDelete)
{
throw;
}
}
}
DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
isRequiredForDelete = false;
}
@@ -463,17 +450,73 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
_itemRepository.DeleteItem(item.Id);
_itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
_cache.TryRemove(item.Id, out _);
foreach (var child in children)
{
_itemRepository.DeleteItem(child.Id);
_cache.TryRemove(child.Id, out _);
}
ReportItemRemoved(item, parent);
}
private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
{
if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
{
try
{
_logger.LogInformation(
"Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
if (fileSystemInfo.IsDirectory)
{
Directory.Delete(fileSystemInfo.FullName, true);
}
else
{
File.Delete(fileSystemInfo.FullName);
}
}
catch (DirectoryNotFoundException)
{
_logger.LogInformation(
"Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
}
catch (FileNotFoundException)
{
_logger.LogInformation(
"File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
}
catch (IOException)
{
if (isRequiredForDelete)
{
throw;
}
}
catch (UnauthorizedAccessException)
{
if (isRequiredForDelete)
{
throw;
}
}
}
}
private bool IsInternalItem(BaseItem item)
{
if (!item.IsFileProtocol)
@@ -826,6 +869,7 @@ namespace Emby.Server.Implementations.Library
if (!folder.ParentId.Equals(rootFolder.Id))
{
rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult();
folder.ParentId = rootFolder.Id;
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult();
}
@@ -989,6 +1033,11 @@ namespace Emby.Server.Implementations.Library
return GetArtist(name, new DtoOptions(true));
}
public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
{
return _itemRepository.FindArtists(names);
}
public MusicArtist GetArtist(string name, DtoOptions options)
{
return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
@@ -1090,6 +1139,7 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{
RootFolder.Children = null;
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
// Start by just validating the children of the root, but go no further
@@ -1100,9 +1150,12 @@ namespace Emby.Server.Implementations.Library
allowRemoveRoot: removeRoot,
cancellationToken: cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
var rootFolder = GetUserRootFolder();
rootFolder.Children = null;
await GetUserRootFolder().ValidateChildren(
await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
await rootFolder.ValidateChildren(
new Progress<double>(),
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false,
@@ -1110,18 +1163,24 @@ namespace Emby.Server.Implementations.Library
cancellationToken: cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
foreach (var child in GetUserRootFolder().Children.OfType<Folder>())
var toDelete = new List<Guid>();
foreach (var child in rootFolder.Children!.OfType<Folder>())
{
// If the user has somehow deleted the collection directory, remove the metadata from the database.
if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
{
_itemRepository.DeleteItem(collectionFolder.Id);
toDelete.Add(collectionFolder.Id);
}
else
{
await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
}
if (toDelete.Count > 0)
{
_itemRepository.DeleteItem(toDelete.ToArray());
}
}
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
@@ -2027,6 +2086,12 @@ namespace Emby.Server.Implementations.Library
}
}
if (!File.Exists(image.Path))
{
_logger.LogWarning("Image not found at {ImagePath}", image.Path);
continue;
}
ImageDimensions size;
try
{

View File

@@ -657,7 +657,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
_logger.LogDebug(ex, "Error parsing cached media info.");
}
finally
{

View File

@@ -45,11 +45,14 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
{
IncludeItemTypes = [BaseItemKind.Audio],
DtoOptions = dtoOptions
})
.GetRecursiveChildren(
user,
new InternalItemsQuery(user)
{
IncludeItemTypes = [BaseItemKind.Audio],
DtoOptions = dtoOptions
},
out _)
.Cast<Audio>()
.SelectMany(i => i.Genres)
.Concat(item.Genres)

View File

@@ -405,6 +405,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (child.IsDirectory)
{
if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename))
{
continue;
}
if (IsDvdDirectory(child.FullName, filename, directoryService))
{
var movie = new T

View File

@@ -80,6 +80,7 @@ namespace Emby.Server.Implementations.Library
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
_cache.AddOrUpdate(cacheKey, userData);
item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate the cached userdata
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{
@@ -159,7 +160,7 @@ namespace Emby.Server.Implementations.Library
};
}
private UserItemData Map(UserData dto)
private static UserItemData Map(UserData dto)
{
return new UserItemData()
{
@@ -237,7 +238,10 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item)
{
return GetUserData(user, item.Id, item.GetUserDataKeys());
return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData()
{
Key = item.GetUserDataKeys()[0],
};
}
/// <inheritdoc />
@@ -304,7 +308,7 @@ namespace Emby.Server.Implementations.Library
// ignore progress during the beginning
positionTicks = 0;
}
else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks)
else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPerSecond))
{
// mark as completed close to the end
positionTicks = 0;

View File

@@ -374,13 +374,22 @@ namespace Emby.Server.Implementations.Library
if (request.GroupItems)
{
if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.tvshows))
var collectionType = parents
.Select(parent => parent switch
{
ICollectionFolder collectionFolder => collectionFolder.CollectionType,
UserView userView => userView.CollectionType,
_ => null
})
.FirstOrDefault(type => type is not null);
if (collectionType == CollectionType.tvshows)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
}
if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.music))
if (collectionType == CollectionType.music)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);

View File

@@ -1,5 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -55,6 +55,8 @@ public class PeopleValidator
var numPeople = people.Count;
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
_logger.LogDebug("Will refresh {Amount} people", numPeople);
foreach (var person in people)
@@ -92,7 +94,7 @@ public class PeopleValidator
double percent = numComplete;
percent /= numPeople;
progress.Report(100 * percent);
subProgress.Report(100 * percent);
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
@@ -102,17 +104,13 @@ public class PeopleValidator
IsLocked = false
});
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
var i = 0;
foreach (var item in deadEntities.Chunk(500))
{
_libraryManager.DeleteItemsUnsafeFast(item);
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
}
progress.Report(100);

View File

@@ -1,16 +1,16 @@
{
"Sync": "Сінхранізаваць",
"Playlists": "Спісы прайгравання",
"Latest": "Апошні",
"Playlists": "Плэй-лісты",
"Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
"ItemAddedWithName": "{0} даданы ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны",
"UserCreatedWithName": "Карыстальнік {0} быў створаны",
"Albums": "Альбомы",
"Application": "Прыкладанне",
"AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны",
"Application": "Праграма",
"AuthenticationSucceededWithUserName": "{0} паспяхова аўтарызаваны",
"Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі",
@@ -29,18 +29,18 @@
"HeaderAlbumArtists": "Выканаўцы альбома",
"LabelRunningTimeValue": "Працягласць: {0}",
"HomeVideos": "Хатнія відэа",
"ItemRemovedWithName": "{0} быў выдалены з бібліятэкі",
"MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}",
"ItemRemovedWithName": "{0} выдалены з бібліятэкі",
"MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да версіі {0}",
"Movies": "Фільмы",
"Music": "Музыка",
"MusicVideos": "Музычныя кліпы",
"NameInstallFailed": "Устаноўка {0} не атрымалася",
"NameInstallFailed": "Усталяванне {0} не атрымалася",
"NameSeasonNumber": "Сезон {0}",
"NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання",
"NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне праграмы",
"NotificationOptionPluginInstalled": "Плагін усталяваны",
"NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана",
"NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна ўсталявана",
"NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера",
"Photos": атаграфіі",
"Photos": отаздымкі",
"Plugin": "Плагін",
"PluginUninstalledWithName": "{0} быў выдалены",
"PluginUpdatedWithName": "{0} быў абноўлены",
@@ -54,16 +54,16 @@
"Artists": "Выканаўцы",
"UserOfflineFromDevice": "{0} адлучыўся ад {1}",
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
"TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.",
"TaskCleanActivityLogDescription": "Выдаляе запісы старэйшыя за зададзены ўзрост ў журнале актыўнасці.",
"TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.",
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.",
"TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.",
"TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.",
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метададзеных.",
"TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых зменаў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць выдайнасць.",
"TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
"TasksApplicationCategory": "Прыкладанне",
"AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}",
"TasksApplicationCategory": "Праграма",
"AppDeviceValues": "Праграма: {0}, Прылада: {1}",
"Books": "Кнігі",
"CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
"DeviceOfflineWithName": "{0} адлучыўся",
@@ -74,7 +74,7 @@
"HeaderFavoriteArtists": "Абраныя выканаўцы",
"HearingImpaired": "Са слабым слыхам",
"Inherit": "Атрымаць у спадчыну",
"MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена",
"MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера (секцыя {0}) абноўлена",
"MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена",
"MixedContent": "Змешаны змест",
"NameSeasonUnknown": "Невядомы сезон",
@@ -92,48 +92,48 @@
"NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена",
"ScheduledTaskFailedWithName": "{0} не атрымалася",
"ScheduledTaskStartedWithName": "{0} пачалося",
"ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць",
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
"Shows": "Шоу",
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
"SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
"TvShows": "ТБ-шоу",
"TvShows": "Тэлепраграма",
"Undefined": "Нявызначана",
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
"UserOnlineFromDevice": "{0} падключаны з {1}",
"UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}",
"UserStartedPlayingItemWithValues": "{0} грае {1} на {2}",
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
"ValueSpecialEpisodeName": "Спецэпізод - {0}",
"VersionNumber": "Версія {0}",
"TasksMaintenanceCategory": "Абслугоўванне",
"TasksLibraryCategory": "Медыятэка",
"TasksLibraryCategory": "Бібліятэка",
"TasksChannelsCategory": "Інтэрнэт-каналы",
"TaskCleanActivityLog": "Ачысціць журнал актыўнасці",
"TaskCleanCache": "Ачысціць кэш",
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
"TaskRefreshChapterImages": "Выняць выявы раздзелаў",
"TaskRefreshLibrary": "Сканіраваць медыятэку",
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
"TaskCleanLogs": "Ачысціць часопіс",
"TaskRefreshPeople": "Абнавіць людзей",
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
"TaskRefreshLibrary": "Сканаваць бібліятэку",
"TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
"TaskCleanLogs": "Ачысціць журнал",
"TaskRefreshPeople": "Абнавіць выканаўцаў",
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
"TaskUpdatePlugins": "Абнавіць плагіны",
"TaskCleanTranscode": "Ачысціць каталог перакадзіравання",
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.",
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
"TaskAudioNormalization": "Нармалізацыя гуку",
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Перамяшчае існуючыя файлы trickplay у адпаведнасці з наладамі бібліятэкі.",
"TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
"TaskDownloadMissingLyrics": "Спампаваць адсутныя тэксты песняў",
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў",
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",

View File

@@ -15,7 +15,7 @@
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
"Genres": "জনরা",
"Genres": "ধরণ",
"Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
@@ -39,8 +39,8 @@
"Sync": "সমন্বয় করুন",
"SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
"Songs": "সঙ্গীতসমূহ",
"Shows": "টিভি পর্ব",
"Songs": "সঙ্গীত সমূহ",
"Shows": "শো সমূহ",
"ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
"ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
"ScheduledTaskFailedWithName": "{0} ব্যর্থ",
@@ -51,9 +51,9 @@
"Plugin": "প্লাগিন",
"Playlists": "প্লে লিস্ট সমূহ",
"Photos": "ছবিসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে",
"NotificationOptionVideoPlayback": "ভিডিও শুরু হেছে",
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
"NotificationOptionVideoPlaybackStopped": "ভিডিও প্লেব্যাক বন্ধ হয়েছে",
"NotificationOptionVideoPlayback": "ভিডিও প্লেব্যাক শুরু হয়েছে",
"NotificationOptionUserLockedOut": "ব্যবহারকারী লক আউট হয়েছে",
"NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে",
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে",
@@ -85,7 +85,7 @@
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "মূল থেকে গ্রহণ করুন",
"Inherit": "উত্তরাধিকারসূত্র থেকে গ্রহণ করুন",
"HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
@@ -126,16 +126,16 @@
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
"TaskDownloadMissingLyricsDescription": "গানের লিরিকস ডাউনলোড কর",
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
"TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।",
"TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।",
"CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।",
"TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন",
"TaskAudioNormalization": "অডিও নর্মলাইজেশন",
"CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ"
"CleanupUserDataTask": "ইউজার ডেটা ক্লিনআপ কাজ"
}

View File

@@ -138,5 +138,5 @@
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Anschaustatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
}

View File

@@ -136,5 +136,7 @@
"TaskExtractMediaSegments": "Escanear Segmentos de Media",
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medio de plugins habilitados para MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Mueve archivos existentes de trickplay de acuerdo a la configuración de la biblioteca.",
"TaskMoveTrickplayImages": "Migrar Ubicación de Imagen de Trickplay"
"TaskMoveTrickplayImages": "Migrar Ubicación de Imagen de Trickplay",
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, estado de los favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
"CleanupUserDataTask": "Tarea de limpieza de datos de usuarios"
}

View File

@@ -137,5 +137,6 @@
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
"CleanupUserDataTask": "Tarea de limpieza de los datos del usuario"
"CleanupUserDataTask": "Tarea de limpieza de los datos del usuario",
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días."
}

View File

@@ -125,5 +125,11 @@
"Undefined": "Sin definir",
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
"TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
"TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad."
"TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.",
"NotificationOptionApplicationUpdateAvailable": "actualización disponible",
"TaskDownloadMissingLyrics": "Descargue letras desaparecidas",
"TaskDownloadMissingLyricsDescription": "Decarga letras para canciones",
"TaskMoveTrickplayImages": "Mover localización de foto vista previa",
"NotificationOptionApplicationUpdateInstalled": "Aplicación actualización disponible",
"CleanupUserDataTask": "Tarea de limpieza de los datos del usuario"
}

View File

@@ -69,7 +69,7 @@
"TvShows": "Sarjat",
"Sync": "Synkronointi",
"SubtitleDownloadFailureFromForItem": "Tekstityksen lataus lähteestä \"{0}\" kohteelle \"{1}\" epäonnistui",
"StartupEmbyServerIsLoading": "Jellyfin-palvelin latautuu. Yritä hetken kuluttua uudelleen.",
"StartupEmbyServerIsLoading": "Jellyfin-palvelin on latautumassa. Yritä hetken kuluttua uudelleen.",
"Songs": "Kappaleet",
"Shows": "Sarjat",
"ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
@@ -79,7 +79,7 @@
"NotificationOptionVideoPlayback": "Videon toisto aloitettu",
"NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
"NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui",
"NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys",
"NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan",
"NotificationOptionPluginUpdateInstalled": "Lisäosa päivitettiin",
"NotificationOptionPluginUninstalled": "Lisäosa poistettiin",
"NotificationOptionPluginInstalled": "Lisäosa asennettiin",

View File

@@ -136,5 +136,6 @@
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario"
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario",
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (Estado de visualización, estado de favorito, etc) da multimedia que leve non presente polo menos durante 90 días."
}

View File

@@ -12,10 +12,10 @@
"DeviceOfflineWithName": "{0} wurde getrennt",
"DeviceOnlineWithName": "{0} ist verbunden",
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
"Favorites": "Favoriten",
"Favorites": "Favorite",
"Folders": "Ordner",
"Genres": "Genre",
"HeaderAlbumArtists": "Album-Künstler",
"HeaderAlbumArtists": "Album-Künschtler",
"HeaderContinueWatching": "weiter schauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Künstler",

View File

@@ -136,5 +136,7 @@
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
"TaskMoveTrickplayImages": "Preseli lokaciju Trickplay slika",
"TaskMoveTrickplayImagesDescription": "Preseli lokaciju Trickplay slika prema postavkama zbirke."
"TaskMoveTrickplayImagesDescription": "Preseli lokaciju Trickplay slika prema postavkama zbirke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
}

View File

@@ -136,5 +136,7 @@
"TaskExtractMediaSegments": "Skann mediasegment",
"TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay",
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment."
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment.",
"CleanupUserDataTaskDescription": "Sletter all brukerdata (avspillings-status, favoritter osv.) fra innhold som har vært utilgjengelig i minst 90 dager.",
"CleanupUserDataTask": "Oppgave for opprydding av brukerdata"
}

View File

@@ -41,5 +41,77 @@
"MixedContent": "Jumbled loot",
"Music": "Tunes",
"NameInstallFailed": "Ye couldn't bring {0} aboard yer ship",
"MessageApplicationUpdatedTo": "Yer Map of the Seas has been scribbled with {0}"
"MessageApplicationUpdatedTo": "Yer Map of the Seas has been scribbled with {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Yer Map Drawer has been rescribbled to {0}",
"MessageServerConfigurationUpdated": "Yer Map drawer has been rescribbled",
"Inherit": "Carry on what be passed along",
"Latest": "Newfangled",
"Movies": "Moving pictures",
"NewVersionIsAvailable": "A fresh build o Jellyfin Server be waitin fer ye to fetch.",
"NotificationOptionPluginInstalled": "Plugin nailed down",
"NotificationOptionVideoPlayback": "Video playback be underway",
"ScheduledTaskFailedWithName": "{0} ran aground",
"StartupEmbyServerIsLoading": "Jellyfin Server be preparin the ship. Try yer luck again soon.",
"UserOfflineFromDevice": "{0} severed ties with {1}",
"UserDownloadingItemWithValues": "{0} be haulin in {1}",
"UserStartedPlayingItemWithValues": "{0} be playin {1} aboard {2}",
"ValueHasBeenAddedToLibrary": "{0} be stashed in yer treasure trove",
"TaskCleanCacheDescription": "Wipes away cache cargo no longer called fer.",
"TaskCleanLogsDescription": "Clears the logbook o entries older than {0} days.",
"TaskRefreshPeopleDescription": "Refreshes the charts fer actors an directors in yer Treasure Trove.",
"UserLockedOutWithName": "Matey {0} be denied boarding",
"TaskAudioNormalization": "Steadyin the shanties",
"TaskAudioNormalizationDescription": "Scans files fer shanty steadiyin data.",
"HeaderRecordingGroups": "Loggin' Groups",
"MusicVideos": "Shanty films",
"Playlists": "Lists o plunder",
"Plugin": "Extra sail",
"NotificationOptionVideoPlaybackStopped": "Video playback dropped anchor",
"NameSeasonNumber": "Saga {0}",
"NameSeasonUnknown": "Saga be Lost",
"NotificationOptionApplicationUpdateAvailable": "A fresh build awaits",
"NotificationOptionApplicationUpdateInstalled": "App upgrade be aboard",
"NotificationOptionAudioPlayback": "Audio playback be rollin",
"NotificationOptionAudioPlaybackStopped": "Audio playback dropped anchor",
"NotificationOptionCameraImageUploaded": "Spyglass shot be hoisted",
"NotificationOptionInstallationFailed": "Install be wrecked",
"NotificationOptionNewLibraryContent": "Fresh plunder ready to claim",
"NotificationOptionPluginError": "Plugin ran aground",
"NotificationOptionPluginUninstalled": "Plugin cast overboard",
"NotificationOptionPluginUpdateInstalled": "Plugin patched n ready",
"NotificationOptionServerRestartRequired": "Server be due fer a restart",
"NotificationOptionTaskFailed": "Set chore went overboard",
"TaskRefreshLibraryDescription": "Searches the Treasure Trove fer new plunder n updates the charts.",
"PluginInstalledWithName": "{0} nailed down",
"TaskCleanLogs": "Swab the Log Hold",
"TaskRefreshPeople": "Freshen the Mateys",
"PluginUninstalledWithName": "{0} sent t Davy Jones",
"PluginUpdatedWithName": "{0} patched n ready",
"ProviderValue": "Supplier o goods: {0}",
"ScheduledTaskStartedWithName": "{0} set sail",
"ServerNameNeedsToBeRestarted": "{0} be cravin a restart",
"Shows": "Sagas",
"SubtitleDownloadFailureFromForItem": "Subtitles be sunk fetchin from {0} fer {1}",
"Sync": "Match the tides",
"System": "The ships works",
"TvShows": "TV Sagas",
"Undefined": "Uncharted",
"User": "Matey",
"UserCreatedWithName": "Matey {0} joined the crew",
"UserDeletedWithName": "Matey {0} cast overboard",
"UserOnlineFromDevice": "{0} be aboard ship from {1}",
"UserPasswordChangedWithName": "New passphrase set fer Matey {0}",
"UserPolicyUpdatedWithName": "Ship rules be changed fer {0}",
"UserStoppedPlayingItemWithValues": "{0} be done playin {1} on {2",
"ValueSpecialEpisodeName": "Special Tale {0}",
"VersionNumber": "Edition {0}",
"TasksMaintenanceCategory": "Hull patchin",
"TasksLibraryCategory": "Treasure Trove",
"TasksApplicationCategory": "Ship",
"TaskCleanActivityLog": "Clear the Ships Log",
"TaskCleanActivityLogDescription": "Purges ships logs older than the chosen time.",
"TaskCleanCache": "Sweep the Cache Chest",
"TaskRefreshChapterImages": "Claim chapter portraits",
"TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.",
"TaskRefreshLibrary": "Scan the Treasure Trove"
}

View File

@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} - неудачна",
"ScheduledTaskStartedWithName": "{0} - запущена",
"ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}",
"Shows": "Телешоу",
"Shows": "Сериалы",
"Songs": "Композиции",
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",

View File

@@ -135,5 +135,7 @@
"TaskCleanCollectionsAndPlaylists": "Pastron koleksionet dhe listat e këngëve",
"TaskCleanCollectionsAndPlaylistsDescription": "Heq elementet nga koleksionet dhe listat e këngëve që nuk ekzistojnë më.",
"TaskAudioNormalization": "Normalizimi i audios",
"TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios."
"TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios.",
"CleanupUserDataTaskDescription": "Pastron të gjitha të dhënat e përdorueseve (gjendja e shikimit, statusi i të preferuarave etj.) nga mediat që nuk janë më të pranishme për të paktën 90 ditë.",
"CleanupUserDataTask": "Veprim për pastrimin të dhënave të përdorueseve"
}

View File

@@ -126,5 +126,16 @@
"HearingImpaired": "ослабљен слух",
"TaskAudioNormalization": "Нормализација звука",
"TaskCleanCollectionsAndPlaylists": "Очистите колекције и плејлисте",
"TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука."
"TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука.",
"TaskRefreshTrickplayImages": "Направи сличице за визуелно премотавање",
"TaskRefreshTrickplayImagesDescription": "Прављење сличица које помажу код визуелног премотавања видео-снимака.",
"TaskDownloadMissingLyrics": "Преузми стихове који недостају",
"TaskCleanCollectionsAndPlaylistsDescription": "Уклања ставке које више не постоје из колекција и плејлиста.",
"TaskExtractMediaSegments": "Скенирај сегменте медија",
"TaskExtractMediaSegmentsDescription": "Извлачи или добавља сегменте медија у додацима који раде са MediaSegment-ом.",
"TaskMoveTrickplayImagesDescription": "Премешта постојеће сличице за визуелно премотавање сходно подешавањима библиотеке.",
"CleanupUserDataTask": "Задатак чишћења корисничких података",
"CleanupUserDataTaskDescription": "Чисти све корисничке податке (напредак гледања, ознаке за омиљено...) медија који нису доступни 90 дана или дуже.",
"TaskMoveTrickplayImages": "Промени локацију сличица за визуелно премотавање",
"TaskDownloadMissingLyricsDescription": "Преузми стихове песама"
}

View File

@@ -59,5 +59,6 @@
"NotificationOptionAudioPlayback": "ఆడియో ప్లే కావడం మొదలైంది",
"NotificationOptionCameraImageUploaded": "కెమెరా చిత్రాన్ని అప్లోడ్ చేశారు",
"NotificationOptionInstallationFailed": "ఇన్స్టాలేషన్ విఫలమైంది",
"NotificationOptionServerRestartRequired": "సర్వర్ రీస్టార్ట్ అవసరం"
"NotificationOptionServerRestartRequired": "సర్వర్ రీస్టార్ట్ అవసరం",
"Inherit": "సంక్రమించు"
}

View File

@@ -402,8 +402,8 @@ sog|||Sogdian|sogdien
som||so|Somali|somali
son|||Songhai languages|songhai, langues
sot||st|Sotho, Southern|sotho du Sud
spa||es-419|Spanish; Latin|espagnol; Latin
spa||es|Spanish; Castilian|espagnol; castillan
spa||es-419|Spanish; Latin|espagnol; Latin
sqi|alb|sq|Albanian|albanais
srd||sc|Sardinian|sarde
srn|||Sranan Tongo|sranan tongo

View File

@@ -314,7 +314,7 @@ namespace Emby.Server.Implementations.Playlists
return;
}
var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1;
var newPriorItemIndex = Math.Max(newIndex - 1, 0);
var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex);

View File

@@ -33,6 +33,8 @@ public partial class AudioNormalizationTask : IScheduledTask
private readonly ILocalizationManager _localization;
private readonly ILogger<AudioNormalizationTask> _logger;
private static readonly TimeSpan _dbSaveInterval = TimeSpan.FromMinutes(5);
/// <summary>
/// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
/// </summary>
@@ -82,7 +84,9 @@ public partial class AudioNormalizationTask : IScheduledTask
foreach (var library in libraries)
{
var startDbSaveInterval = Stopwatch.GetTimestamp();
var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true });
var toSaveDbItems = new List<BaseItem>();
double nextPercent = numComplete + 1;
nextPercent /= libraries.Length;
@@ -114,14 +118,33 @@ public partial class AudioNormalizationTask : IScheduledTask
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
cancellationToken).ConfigureAwait(false);
toSaveDbItems.Add(a);
}
finally
{
File.Delete(tempFile);
try
{
File.Delete(tempFile);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete concat file: {FileName}.", tempFile);
}
}
}
}
if (Stopwatch.GetElapsedTime(startDbSaveInterval) > _dbSaveInterval)
{
if (toSaveDbItems.Count > 1)
{
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
toSaveDbItems.Clear();
}
startDbSaveInterval = Stopwatch.GetTimestamp();
}
// Update sub-progress for album gain
albumComplete++;
double albumPercent = albumComplete;
@@ -133,7 +156,13 @@ public partial class AudioNormalizationTask : IScheduledTask
// Update progress to start at the track gain percent calculation
percent += nextPercent;
_itemRepository.SaveItems(albums, cancellationToken);
if (toSaveDbItems.Count > 1)
{
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
toSaveDbItems.Clear();
}
startDbSaveInterval = Stopwatch.GetTimestamp();
// Track gain
var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true });
@@ -147,6 +176,18 @@ public partial class AudioNormalizationTask : IScheduledTask
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
false,
cancellationToken).ConfigureAwait(false);
toSaveDbItems.Add(t);
}
if (Stopwatch.GetElapsedTime(startDbSaveInterval) > _dbSaveInterval)
{
if (toSaveDbItems.Count > 1)
{
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
toSaveDbItems.Clear();
}
startDbSaveInterval = Stopwatch.GetTimestamp();
}
// Update sub-progress for track gain
@@ -157,7 +198,10 @@ public partial class AudioNormalizationTask : IScheduledTask
progress.Report(100 * (percent + (trackPercent * nextPercent)));
}
_itemRepository.SaveItems(tracks, cancellationToken);
if (toSaveDbItems.Count > 1)
{
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
}
// Update progress
numComplete++;
@@ -195,9 +239,9 @@ public partial class AudioNormalizationTask : IScheduledTask
},
})
{
_logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
try
{
_logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
process.Start();
}
catch (Exception ex)
@@ -206,6 +250,15 @@ public partial class AudioNormalizationTask : IScheduledTask
return null;
}
try
{
process.PriorityClass = ProcessPriorityClass.BelowNormal;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error setting ffmpeg process priority");
}
using var reader = process.StandardError;
float? lufs = null;
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))

View File

@@ -61,7 +61,7 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
yield return new TaskTriggerInfo
{
Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
IntervalTicks = TimeSpan.FromHours(6).Ticks
};
}

View File

@@ -1,10 +1,14 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
@@ -15,16 +19,19 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
/// <summary>
/// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization)
/// <param name="dbContextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param>
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbContextFactory)
{
_libraryManager = libraryManager;
_localization = localization;
_dbContextFactory = dbContextFactory;
}
/// <inheritdoc />
@@ -62,8 +69,61 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
}
/// <inheritdoc />
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
return _libraryManager.ValidatePeopleAsync(progress, cancellationToken);
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false);
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var dupQuery = context.Peoples
.GroupBy(e => new { e.Name, e.PersonType })
.Where(e => e.Count() > 1)
.Select(e => e.Select(f => f.Id).ToArray());
var total = dupQuery.Count();
const int PartitionSize = 100;
var iterator = 0;
int itemCounter;
var buffer = ArrayPool<Guid[]>.Shared.Rent(PartitionSize)!;
try
{
do
{
itemCounter = 0;
await foreach (var item in dupQuery
.Take(PartitionSize)
.AsAsyncEnumerable()
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
buffer[itemCounter++] = item;
}
for (int i = 0; i < itemCounter; i++)
{
var item = buffer[i];
var reference = item[0];
var dups = item[1..];
await context.PeopleBaseItemMap.WhereOneOrMany(dups, e => e.PeopleId)
.ExecuteUpdateAsync(e => e.SetProperty(f => f.PeopleId, reference), cancellationToken)
.ConfigureAwait(false);
await context.Peoples.Where(e => dups.Contains(e.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(100f / total * ((iterator * PartitionSize) + i));
}
iterator++;
} while (itemCounter == PartitionSize && !cancellationToken.IsCancellationRequested);
}
finally
{
ArrayPool<Guid[]>.Shared.Return(buffer);
}
subProgress.Report(100);
}
}
}

View File

@@ -6,7 +6,6 @@ using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
@@ -54,7 +53,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>DateTime.</returns>
private int GetValue(BaseItem x)
{
return x.IsFavoriteOrLiked(User) ? 0 : 1;
return x.IsFavoriteOrLiked(User, userItemData: null) ? 0 : 1;
}
}
}

View File

@@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
@@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>DateTime.</returns>
private int GetValue(BaseItem x)
{
return x.IsPlayed(User) ? 0 : 1;
return x.IsPlayed(User, userItemData: null) ? 0 : 1;
}
}
}

View File

@@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
@@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>DateTime.</returns>
private int GetValue(BaseItem x)
{
return x.IsUnplayed(User) ? 0 : 1;
return x.IsUnplayed(User, userItemData: null) ? 0 : 1;
}
}
}

View File

@@ -96,9 +96,6 @@ public class DisplayPreferencesController : BaseJellyfinApiController
dto.CustomPrefs.TryAdd(key, value);
}
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
_displayPreferencesManager.SaveChanges();
return dto;
}
@@ -210,8 +207,8 @@ public class DisplayPreferencesController : BaseJellyfinApiController
// Set all remaining custom preferences.
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId.Value, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
_displayPreferencesManager.SaveChanges();
_displayPreferencesManager.UpdateItemDisplayPreferences(itemPrefs);
_displayPreferencesManager.UpdateDisplayPreferences(existingDisplayPreferences);
return NoContent();
}
}

View File

@@ -784,6 +784,7 @@ public class LibraryController : BaseJellyfinApiController
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
EnableGroupByMetadataKey = isMovie ?? false,
ExcludeItemIds = [itemId]
};
// ExcludeArtistIds

View File

@@ -108,6 +108,7 @@ public class YearsController : BaseJellyfinApiController
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
IReadOnlyList<BaseItem> items;
int totalCount = -1;
if (parentItem.IsFolder)
{
var folder = (Folder)parentItem;
@@ -118,7 +119,7 @@ public class YearsController : BaseJellyfinApiController
}
else
{
items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray();
items = recursive ? folder.GetRecursiveChildren(user, query, out totalCount) : folder.GetChildren(user, true).Where(Filter).ToArray();
}
}
else
@@ -153,7 +154,7 @@ public class YearsController : BaseJellyfinApiController
var result = new QueryResult<BaseItemDto>(
startIndex,
ibnItemsArray.Count,
totalCount == -1 ? ibnItemsArray.Count : totalCount,
dtos.Where(i => i is not null).ToArray());
return result;
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data;
using Jellyfin.Database.Implementations.Enums;
@@ -56,6 +57,21 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
return Task.FromResult(_sessionManager.Sessions);
}
/// <inheritdoc />
protected override Task<IEnumerable<SessionInfo>> GetDataToSendForConnection(IWebSocketConnection connection)
{
// For non-admin users, filter the sessions to only include their own sessions
if (connection.AuthorizationInfo?.User is not null &&
!connection.AuthorizationInfo.IsApiKey &&
!connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
{
var userId = connection.AuthorizationInfo.User.Id;
return Task.FromResult(_sessionManager.Sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId)));
}
return Task.FromResult(_sessionManager.Sessions);
}
/// <inheritdoc />
protected override async ValueTask DisposeAsyncCore()
{
@@ -80,11 +96,10 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
/// <param name="message">The message.</param>
protected override void Start(WebSocketMessageInfo message)
{
if (!message.Connection.AuthorizationInfo.IsApiKey
&& (message.Connection.AuthorizationInfo.User is null
|| !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)))
// Allow all authenticated users to subscribe to session information
if (message.Connection.AuthorizationInfo.User is null && !message.Connection.AuthorizationInfo.IsApiKey)
{
throw new AuthenticationException("Only admin users can subscribe to session information.");
throw new AuthenticationException("User must be authenticated to subscribe to session Information.");
}
base.Start(message);

View File

@@ -39,7 +39,7 @@ public class BackupService : IBackupService
ReferenceHandler = ReferenceHandler.IgnoreCycles,
};
private readonly Version _backupEngineVersion = Version.Parse("0.2.0");
private readonly Version _backupEngineVersion = new Version(0, 2, 0);
/// <summary>
/// Initializes a new instance of the <see cref="BackupService"/> class.

View File

@@ -99,11 +99,11 @@ public sealed class BaseItemRepository
}
/// <inheritdoc />
public void DeleteItem(Guid id)
public void DeleteItem(params IReadOnlyList<Guid> ids)
{
if (id.IsEmpty() || id.Equals(PlaceholderId))
if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
{
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(id));
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
}
using var context = _dbProvider.CreateDbContext();
@@ -111,13 +111,15 @@ public sealed class BaseItemRepository
var date = (DateTime?)DateTime.UtcNow;
var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
// Remove any UserData entries for the placeholder item that would conflict with the UserData
// being detached from the item being deleted. This is necessary because, during an update,
// UserData may be reattached to a new entry, but some entries can be left behind.
// Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
context.UserData
.Join(
context.UserData.Where(e => e.ItemId == id),
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
userData => new { userData.UserId, userData.CustomDataKey },
(placeholder, userData) => placeholder)
@@ -125,29 +127,31 @@ public sealed class BaseItemRepository
.ExecuteDelete();
// Detach all user watch data
context.UserData.Where(e => e.ItemId == id)
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteUpdate(e => e
.SetProperty(f => f.RetentionDate, date)
.SetProperty(f => f.ItemId, PlaceholderId));
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distinct().ToArray();
context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.SaveChanges();
transaction.Commit();
}
@@ -267,7 +271,7 @@ public sealed class BaseItemRepository
result.TotalRecordCount = dbQuery.Count();
}
dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
@@ -286,7 +290,7 @@ public sealed class BaseItemRepository
dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
@@ -328,7 +332,7 @@ public sealed class BaseItemRepository
var mainquery = PrepareItemQuery(context, filter);
mainquery = TranslateQuery(mainquery, context, filter);
mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
mainquery = ApplyGroupingFilter(mainquery, filter);
mainquery = ApplyGroupingFilter(context, mainquery, filter);
mainquery = ApplyQueryPaging(mainquery, filter);
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
@@ -365,37 +369,53 @@ public sealed class BaseItemRepository
return query.ToArray();
}
private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
// This whole block is needed to filter duplicate entries on request
// for the time being it cannot be used because it would destroy the ordering
// this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but
// for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
// var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
// if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
// {
// dbQuery = ApplyOrder(dbQuery, filter);
// dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First());
// }
// else if (enableGroupByPresentationUniqueKey)
// {
// dbQuery = ApplyOrder(dbQuery, filter);
// dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First());
// }
// else if (filter.GroupBySeriesPresentationUniqueKey)
// {
// dbQuery = ApplyOrder(dbQuery, filter);
// dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First());
// }
// else
// {
// dbQuery = dbQuery.Distinct();
// dbQuery = ApplyOrder(dbQuery, filter);
// }
dbQuery = dbQuery.Distinct();
var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
{
var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
}
else if (enableGroupByPresentationUniqueKey)
{
var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
}
else if (filter.GroupBySeriesPresentationUniqueKey)
{
var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
}
else
{
dbQuery = dbQuery.Distinct();
}
dbQuery = ApplyOrder(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery;
}
private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
dbQuery = dbQuery.Include(e => e.TrailerTypes)
.Include(e => e.Provider)
.Include(e => e.LockedFields)
.Include(e => e.UserData);
if (filter.DtoOptions.EnableImages)
{
dbQuery = dbQuery.Include(e => e.Images);
}
return dbQuery;
}
@@ -422,8 +442,7 @@ public sealed class BaseItemRepository
private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
{
dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyOrder(dbQuery, filter);
dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
return dbQuery;
}
@@ -431,15 +450,7 @@ public sealed class BaseItemRepository
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
{
IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
dbQuery = dbQuery.AsSingleQuery()
.Include(e => e.TrailerTypes)
.Include(e => e.Provider)
.Include(e => e.LockedFields);
if (filter.DtoOptions.EnableImages)
{
dbQuery = dbQuery.Include(e => e.Images);
}
dbQuery = dbQuery.AsSingleQuery();
return dbQuery;
}
@@ -470,7 +481,7 @@ public sealed class BaseItemRepository
var counts = dbQuery
.GroupBy(x => x.Type)
.Select(x => new { x.Key, Count = x.Count() })
.AsEnumerable();
.ToArray();
var lookup = _itemTypeLookup.BaseItemKindNames;
var result = new ItemCounts();
@@ -724,13 +735,20 @@ public sealed class BaseItemRepository
}
using var context = _dbProvider.CreateDbContext();
var item = PrepareItemQuery(context, new()
var dbQuery = PrepareItemQuery(context, new()
{
DtoOptions = new()
{
EnableImages = true
}
}).FirstOrDefault(e => e.Id == id);
});
dbQuery = dbQuery.Include(e => e.TrailerTypes)
.Include(e => e.Provider)
.Include(e => e.LockedFields)
.Include(e => e.UserData)
.Include(e => e.Images);
var item = dbQuery.FirstOrDefault(e => e.Id == id);
if (item is null)
{
return null;
@@ -745,8 +763,9 @@ public sealed class BaseItemRepository
/// <param name="entity">The entity.</param>
/// <param name="dto">The dto base instance.</param>
/// <param name="appHost">The Application server Host.</param>
/// <param name="logger">The applogger.</param>
/// <returns>The dto to map.</returns>
public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logger)
{
dto.Id = entity.Id;
dto.ParentId = entity.ParentId.GetValueOrDefault();
@@ -791,6 +810,8 @@ public sealed class BaseItemRepository
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
dto.Width = entity.Width.GetValueOrDefault();
dto.Height = entity.Height.GetValueOrDefault();
dto.UserData = entity.UserData;
if (entity.Provider is not null)
{
dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
@@ -1144,7 +1165,7 @@ public sealed class BaseItemRepository
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
}
return Map(baseItemEntity, dto, appHost);
return Map(baseItemEntity, dto, appHost, logger);
}
private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
@@ -1302,7 +1323,13 @@ public sealed class BaseItemRepository
result.Items =
[
.. query
.Select(e => e.First())
.Select(e => e.AsQueryable()
.Include(e => e.TrailerTypes)
.Include(e => e.Provider)
.Include(e => e.LockedFields)
.Include(e => e.Images)
.AsSingleQuery()
.First())
.AsEnumerable()
.Where(e => e is not null)
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
@@ -1884,7 +1911,7 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
{
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.NameStartsWith));
baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith));
}
if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
@@ -2270,8 +2297,18 @@ public sealed class BaseItemRepository
if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
{
var include = filter.HasAnyProviderId.Select(e => $"{e.Key}:{e.Value}").ToArray();
baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => include.Contains(f)));
// Allow setting a null or empty value to get all items that have the specified provider set.
var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArray();
if (includeAny.Length > 0)
{
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
}
var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Key}:{e.Value}").ToArray();
if (includeSelected.Length > 0)
{
baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => includeSelected.Contains(f)));
}
}
if (filter.HasImdbId.HasValue)
@@ -2449,4 +2486,68 @@ public sealed class BaseItemRepository
return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
}
}
/// <inheritdoc/>
public bool GetIsPlayed(User user, Guid id, bool recursive)
{
using var dbContext = _dbProvider.CreateDbContext();
if (recursive)
{
var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
return dbContext.BaseItems
.Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
.All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
}
return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
}
private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseItemEntity, bool>>? filter = null)
{
var folderStack = new HashSet<Guid>()
{
parentId
};
var folderList = new HashSet<Guid>()
{
parentId
};
while (folderStack.Count != 0)
{
var items = folderStack.ToArray();
folderStack.Clear();
var query = dbContext.BaseItems
.WhereOneOrMany(items, e => e.ParentId!.Value);
if (filter != null)
{
query = query.Where(filter);
}
foreach (var item in query.Select(e => e.Id).ToArray())
{
if (folderList.Add(item))
{
folderStack.Add(item);
}
}
}
return folderList;
}
/// <inheritdoc/>
public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
{
using var dbContext = _dbProvider.CreateDbContext();
var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!)
.Where(e => artistNames.Contains(e.Name))
.ToArray();
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
}
}

View File

@@ -55,11 +55,14 @@ public class KeyframeRepository : IKeyframeRepository
public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
{
using var context = _dbProvider.CreateDbContext();
using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />

View File

@@ -35,16 +35,22 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter);
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
if (filter.Limit > 0)
{
dbQuery = dbQuery.Take(filter.Limit);
}
// Include PeopleBaseItemMap
if (!filter.ItemId.IsEmpty())
{
dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId));
dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId))
.OrderBy(e => e.BaseItems!.First(e => e.ItemId == filter.ItemId).ListOrder)
.ThenBy(e => e.PersonType)
.ThenBy(e => e.Name);
}
else
{
dbQuery = dbQuery.OrderBy(e => e.Name);
}
if (filter.Limit > 0)
{
dbQuery = dbQuery.Take(filter.Limit);
}
return dbQuery.AsEnumerable().Select(Map).ToArray();
@@ -68,19 +74,42 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
/// <inheritdoc />
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
{
using var context = _dbProvider.CreateDbContext();
foreach (var item in people.Where(e => e.Role is null))
{
item.Role = string.Empty;
}
// TODO: yes for __SOME__ reason there can be duplicates.
people = people.DistinctBy(e => e.Id).ToArray();
var personids = people.Select(f => f.Id);
var existingPersons = context.Peoples.Where(p => personids.Contains(p.Id)).Select(f => f.Id).ToArray();
context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map));
// multiple metadata providers can provide the _same_ person
people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray();
var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray();
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
var existingPersons = context.Peoples.Select(e => new
{
item = e,
SelectionKey = e.Name + "-" + e.PersonType
})
.Where(p => personKeys.Contains(p.SelectionKey))
.Select(f => f.item)
.ToArray();
var toAdd = people
.Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
.Select(Map);
context.Peoples.AddRange(toAdd);
context.SaveChanges();
var maps = context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ToList();
var personsEntities = toAdd.Concat(existingPersons).ToArray();
var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList();
var listOrder = 0;
foreach (var person in people)
{
var existingMap = maps.FirstOrDefault(e => e.PeopleId == person.Id);
var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
if (existingMap is null)
{
context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
@@ -88,22 +117,28 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
Item = null!,
ItemId = itemId,
People = null!,
PeopleId = person.Id,
ListOrder = person.SortOrder,
PeopleId = entityPerson.Id,
ListOrder = listOrder,
SortOrder = person.SortOrder,
Role = person.Role
});
}
else
{
// Update the order for existing mappings
existingMap.ListOrder = listOrder;
existingMap.SortOrder = person.SortOrder;
// person mapping already exists so remove from list
maps.Remove(existingMap);
existingMaps.Remove(existingMap);
}
listOrder++;
}
context.PeopleBaseItemMap.RemoveRange(maps);
context.PeopleBaseItemMap.RemoveRange(existingMaps);
context.SaveChanges();
transaction.Commit();
}
private PersonInfo Map(People people)

View File

@@ -68,86 +68,88 @@ public class MediaSegmentManager : IMediaSegmentManager
return;
}
using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count);
if (forceOverwrite)
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
// delete all existing media segments if forceOverwrite is set.
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
}
_logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count);
foreach (var provider in providers)
{
if (!await provider.Supports(baseItem).ConfigureAwait(false))
{
_logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path);
continue;
}
IQueryable<MediaSegment> existingSegments;
if (forceOverwrite)
{
existingSegments = Array.Empty<MediaSegment>().AsQueryable();
}
else
{
existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name));
// delete all existing media segments if forceOverwrite is set.
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
}
var requestItem = new MediaSegmentGenerationRequest()
foreach (var provider in providers)
{
ItemId = baseItem.Id,
ExistingSegments = existingSegments.Select(e => Map(e)).ToArray()
};
try
{
var segments = await provider.GetMediaSegments(requestItem, cancellationToken)
.ConfigureAwait(false);
if (!forceOverwrite)
if (!await provider.Supports(baseItem).ConfigureAwait(false))
{
var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items.
if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f =>
_logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path);
continue;
}
IQueryable<MediaSegment> existingSegments;
if (forceOverwrite)
{
existingSegments = Array.Empty<MediaSegment>().AsQueryable();
}
else
{
existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name));
}
var requestItem = new MediaSegmentGenerationRequest()
{
ItemId = baseItem.Id,
ExistingSegments = existingSegments.Select(e => Map(e)).ToArray()
};
try
{
var segments = await provider.GetMediaSegments(requestItem, cancellationToken)
.ConfigureAwait(false);
if (!forceOverwrite)
{
return
e.StartTicks == f.StartTicks &&
e.EndTicks == f.EndTicks &&
e.Type == f.Type;
})))
var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items.
if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f =>
{
return
e.StartTicks == f.StartTicks &&
e.EndTicks == f.EndTicks &&
e.Type == f.Type;
})))
{
_logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path);
continue;
}
// delete existing media segments that were re-generated.
await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
}
if (segments.Count == 0 && !requestItem.ExistingSegments.Any())
{
_logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path);
_logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path);
continue;
}
else if (segments.Count == 0 && requestItem.ExistingSegments.Any())
{
_logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path);
continue;
}
// delete existing media segments that were re-generated.
await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path);
var providerId = GetProviderId(provider.Name);
foreach (var segment in segments)
{
segment.ItemId = baseItem.Id;
await CreateSegmentAsync(segment, providerId).ConfigureAwait(false);
}
}
if (segments.Count == 0 && !requestItem.ExistingSegments.Any())
catch (Exception ex)
{
_logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path);
continue;
_logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path);
}
else if (segments.Count == 0 && requestItem.ExistingSegments.Any())
{
_logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path);
continue;
}
_logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path);
var providerId = GetProviderId(provider.Name);
foreach (var segment in segments)
{
segment.ItemId = baseItem.Id;
await CreateSegmentAsync(segment, providerId).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path);
}
}
}
@@ -157,24 +159,34 @@ public class MediaSegmentManager : IMediaSegmentManager
{
ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks);
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
db.MediaSegments.Add(Map(mediaSegment, segmentProviderId));
await db.SaveChangesAsync().ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
db.MediaSegments.Add(Map(mediaSegment, segmentProviderId));
await db.SaveChangesAsync().ConfigureAwait(false);
}
return mediaSegment;
}
/// <inheritdoc />
public async Task DeleteSegmentAsync(Guid segmentId)
{
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
}
}
/// <inheritdoc />
public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
{
using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
@@ -186,36 +198,38 @@ public class MediaSegmentManager : IMediaSegmentManager
return [];
}
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var query = db.MediaSegments
.Where(e => e.ItemId.Equals(item.Id));
if (typeFilter is not null)
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
query = query.Where(e => typeFilter.Contains(e.Type));
}
var query = db.MediaSegments
.Where(e => e.ItemId.Equals(item.Id));
if (filterByProvider)
{
var providerIds = _segmentProviders
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
.Select(f => GetProviderId(f.Name))
.ToArray();
if (providerIds.Length == 0)
if (typeFilter is not null)
{
return [];
query = query.Where(e => typeFilter.Contains(e.Type));
}
query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
}
if (filterByProvider)
{
var providerIds = _segmentProviders
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
.Select(f => GetProviderId(f.Name))
.ToArray();
if (providerIds.Length == 0)
{
return [];
}
return query
.OrderBy(e => e.StartTicks)
.AsNoTracking()
.AsEnumerable()
.Select(Map)
.ToArray();
query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
}
return query
.OrderBy(e => e.StartTicks)
.AsNoTracking()
.AsEnumerable()
.Select(Map)
.ToArray();
}
}
private static MediaSegmentDto Map(MediaSegment segment)

View File

@@ -1,109 +1,116 @@
#pragma warning disable CA1307
#pragma warning disable CA1309
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller;
using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Users
namespace Jellyfin.Server.Implementations.Users;
/// <summary>
/// Manages the storage and retrieval of display preferences through Entity Framework.
/// </summary>
public sealed class DisplayPreferencesManager : IDisplayPreferencesManager
{
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
/// <summary>
/// Manages the storage and retrieval of display preferences through Entity Framework.
/// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
/// </summary>
public sealed class DisplayPreferencesManager : IDisplayPreferencesManager, IAsyncDisposable
/// <param name="dbContextFactory">The database context factory.</param>
public DisplayPreferencesManager(IDbContextFactory<JellyfinDbContext> dbContextFactory)
{
private readonly JellyfinDbContext _dbContext;
_dbContextFactory = dbContextFactory;
}
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
/// </summary>
/// <param name="dbContextFactory">The database context factory.</param>
public DisplayPreferencesManager(IDbContextFactory<JellyfinDbContext> dbContextFactory)
/// <inheritdoc />
public DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client)
{
using var dbContext = _dbContextFactory.CreateDbContext();
var prefs = dbContext.DisplayPreferences
.Include(pref => pref.HomeSections)
.FirstOrDefault(pref =>
pref.UserId.Equals(userId) && pref.Client == client && pref.ItemId.Equals(itemId));
if (prefs is null)
{
_dbContext = dbContextFactory.CreateDbContext();
prefs = new DisplayPreferences(userId, itemId, client);
dbContext.DisplayPreferences.Add(prefs);
dbContext.SaveChanges();
}
/// <inheritdoc />
public DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client)
return prefs;
}
/// <inheritdoc />
public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
using var dbContext = _dbContextFactory.CreateDbContext();
var prefs = dbContext.ItemDisplayPreferences
.FirstOrDefault(pref => pref.UserId.Equals(userId) && pref.ItemId.Equals(itemId) && pref.Client == client);
if (prefs is null)
{
var prefs = _dbContext.DisplayPreferences
.Include(pref => pref.HomeSections)
.FirstOrDefault(pref =>
pref.UserId.Equals(userId) && string.Equals(pref.Client, client) && pref.ItemId.Equals(itemId));
if (prefs is null)
{
prefs = new DisplayPreferences(userId, itemId, client);
_dbContext.DisplayPreferences.Add(prefs);
}
return prefs;
prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
dbContext.ItemDisplayPreferences.Add(prefs);
dbContext.SaveChanges();
}
/// <inheritdoc />
public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
return prefs;
}
/// <inheritdoc />
public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
{
using var dbContext = _dbContextFactory.CreateDbContext();
return dbContext.ItemDisplayPreferences
.Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && prefs.Client == client)
.ToList();
}
/// <inheritdoc />
public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
using var dbContext = _dbContextFactory.CreateDbContext();
return dbContext.CustomItemDisplayPreferences
.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& prefs.Client == client)
.ToDictionary(prefs => prefs.Key, prefs => prefs.Value);
}
/// <inheritdoc />
public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences)
{
using var dbContext = _dbContextFactory.CreateDbContext();
dbContext.CustomItemDisplayPreferences.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& prefs.Client == client)
.ExecuteDelete();
foreach (var (key, value) in customPreferences)
{
var prefs = _dbContext.ItemDisplayPreferences
.FirstOrDefault(pref => pref.UserId.Equals(userId) && pref.ItemId.Equals(itemId) && string.Equals(pref.Client, client));
if (prefs is null)
{
prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
_dbContext.ItemDisplayPreferences.Add(prefs);
}
return prefs;
dbContext.CustomItemDisplayPreferences
.Add(new CustomItemDisplayPreferences(userId, itemId, client, key, value));
}
/// <inheritdoc />
public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
{
return _dbContext.ItemDisplayPreferences
.Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && string.Equals(prefs.Client, client))
.ToList();
}
dbContext.SaveChanges();
}
/// <inheritdoc />
public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
return _dbContext.CustomItemDisplayPreferences
.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& string.Equals(prefs.Client, client))
.ToDictionary(prefs => prefs.Key, prefs => prefs.Value);
}
/// <inheritdoc/>
public void UpdateDisplayPreferences(DisplayPreferences displayPreferences)
{
using var dbContext = _dbContextFactory.CreateDbContext();
dbContext.DisplayPreferences.Attach(displayPreferences).State = EntityState.Modified;
dbContext.SaveChanges();
}
/// <inheritdoc />
public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences)
{
var existingPrefs = _dbContext.CustomItemDisplayPreferences
.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& string.Equals(prefs.Client, client));
_dbContext.CustomItemDisplayPreferences.RemoveRange(existingPrefs);
foreach (var (key, value) in customPreferences)
{
_dbContext.CustomItemDisplayPreferences
.Add(new CustomItemDisplayPreferences(userId, itemId, client, key, value));
}
}
/// <inheritdoc />
public void SaveChanges()
{
_dbContext.SaveChanges();
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await _dbContext.DisposeAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public void UpdateItemDisplayPreferences(ItemDisplayPreferences itemDisplayPreferences)
{
using var dbContext = _dbContextFactory.CreateDbContext();
dbContext.ItemDisplayPreferences.Attach(itemDisplayPreferences).State = EntityState.Modified;
dbContext.SaveChanges();
}
}

View File

@@ -272,6 +272,7 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Users.Attach(user);
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
@@ -887,7 +888,8 @@ namespace Jellyfin.Server.Implementations.Users
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{
dbContext.Users.Update(user);
dbContext.Users.Attach(user);
dbContext.Entry(user).State = EntityState.Modified;
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}

View File

@@ -84,7 +84,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IAuthenticationProvider, DefaultAuthenticationProvider>();
serviceCollection.AddSingleton<IAuthenticationProvider, InvalidAuthProvider>();
serviceCollection.AddSingleton<IPasswordResetProvider, DefaultPasswordResetProvider>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();

View File

@@ -175,7 +175,7 @@ namespace Jellyfin.Server.Filters
// Manually generate sync play GroupUpdate messages.
var groupUpdateTypes = typeof(GroupUpdate<>).Assembly.GetTypes()
.Where(t => t.BaseType != null
.Where(t => t.BaseType is not null
&& t.BaseType.IsGenericType
&& t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>))
.ToList();

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -10,27 +8,44 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.Responses.Add("503", new OpenApiResponse()
{
Description = "The server is currently starting or is temporarily not available.",
Headers = new Dictionary<string, OpenApiHeader>()
operation.Responses.Add(
"503",
new OpenApiResponse
{
Description = "The server is currently starting or is temporarily not available.",
Headers = new Dictionary<string, OpenApiHeader>
{
"Retry-After",
new() { AllowEmptyValue = true, Required = false, Description = "A hint for when to retry the operation in full seconds." }
{
"Retry-After", new OpenApiHeader
{
AllowEmptyValue = true,
Required = false,
Description = "A hint for when to retry the operation in full seconds.",
Schema = new OpenApiSchema
{
Type = "integer",
Format = "int32"
}
}
},
{
"Message", new OpenApiHeader
{
AllowEmptyValue = true,
Required = false,
Description = "A short plain-text reason why the server is not available.",
Schema = new OpenApiSchema
{
Type = "string",
Format = "text"
}
}
}
},
Content = new Dictionary<string, OpenApiMediaType>()
{
"Message",
new() { AllowEmptyValue = true, Required = false, Description = "A short plain-text reason why the server is not available." }
{ "text/html", new OpenApiMediaType() }
}
},
Content = new Dictionary<string, OpenApiMediaType>()
{
{
"text/html",
new OpenApiMediaType()
}
}
});
});
}
}

View File

@@ -62,7 +62,7 @@ internal class JellyfinMigrationService
#pragma warning disable CS0618 // Type or member is obsolete
Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e))
.Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>(), Backup: e.GetCustomAttributes<JellyfinMigrationBackupAttribute>()))
.Where(e => e.Metadata != null)
.Where(e => e.Metadata is not null)
.GroupBy(e => e.Metadata!.Stage)
.Select(f =>
{
@@ -137,7 +137,7 @@ internal class JellyfinMigrationService
var migrationOptions = File.Exists(migrationConfigPath)
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
: null;
if (migrationOptions != null && migrationOptions.Applied.Count > 0)
if (migrationOptions is not null && migrationOptions.Applied.Count > 0)
{
logger.LogInformation("Old migration style migration.xml detected. Migrate now.");
try
@@ -383,7 +383,7 @@ internal class JellyfinMigrationService
}
}
if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider != null)
if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider is not null)
{
logger.LogInformation("A migration will attempt to modify the jellyfin.db, will attempt to backup the file now.");
_backupKey = (_backupKey.LibraryDb, await _jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false), _backupKey.FullBackup);

View File

@@ -41,14 +41,17 @@ public class FixDates : IAsyncMigrationRoutine
{
if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
{
using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var sw = Stopwatch.StartNew();
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var sw = Stopwatch.StartNew();
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
sw.Reset();
await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
sw.Reset();
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
sw.Reset();
await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
sw.Reset();
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -99,6 +99,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
var baseItemIds = new HashSet<Guid>();
using (var operation = GetPreparedDbContext("Moving TypedBaseItem"))
{
IDictionary<Guid, (BaseItemEntity BaseItem, string[] Keys)> allItemsLookup = new Dictionary<Guid, (BaseItemEntity BaseItem, string[] Keys)>();
const string typedBaseItemsQuery =
"""
SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
@@ -115,12 +116,49 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
{
var baseItem = GetItem(dto);
operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem);
baseItemIds.Add(baseItem.BaseItem.Id);
foreach (var dataKey in baseItem.LegacyUserDataKey)
allItemsLookup.Add(baseItem.BaseItem.Id, baseItem);
}
}
bool DoesResolve(Guid? parentId, HashSet<(BaseItemEntity BaseItem, string[] Keys)> checkStack)
{
if (parentId is null)
{
return true;
}
if (!allItemsLookup.TryGetValue(parentId.Value, out var parent))
{
return false; // item is detached and has no root anymore.
}
if (!checkStack.Add(parent))
{
return false; // recursive structure. Abort.
}
return DoesResolve(parent.BaseItem.ParentId, checkStack);
}
using (new TrackedMigrationStep("Clean TypedBaseItems hierarchy", _logger))
{
var checkStack = new HashSet<(BaseItemEntity BaseItem, string[] Keys)>();
foreach (var item in allItemsLookup)
{
var cachedItem = item.Value;
if (DoesResolve(cachedItem.BaseItem.ParentId, checkStack))
{
legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
checkStack.Add(cachedItem);
operation.JellyfinDbContext.BaseItems.Add(cachedItem.BaseItem);
baseItemIds.Add(cachedItem.BaseItem.Id);
foreach (var dataKey in cachedItem.Keys)
{
legacyBaseItemWithUserKeys[dataKey] = cachedItem.BaseItem;
}
}
checkStack.Clear();
}
}
@@ -128,6 +166,8 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
{
operation.JellyfinDbContext.SaveChanges();
}
allItemsLookup.Clear();
}
using (var operation = GetPreparedDbContext("Moving ItemValues"))
@@ -146,6 +186,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
{
var itemId = dto.GetGuid(0);
if (!baseItemIds.Contains(itemId))
{
continue;
}
var entity = GetItemValue(dto);
var key = ((int)entity.Type, entity.Value);
if (!localItems.TryGetValue(key, out var existing))
@@ -279,7 +324,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
{
const string personsQuery =
"""
SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
SELECT ItemId, Name, Role, PersonType, SortOrder, ListOrder FROM People
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
""";
@@ -297,9 +342,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
var entity = GetPerson(reader);
if (!peopleCache.TryGetValue(entity.Name, out var personCache))
if (!peopleCache.TryGetValue(entity.Name + "|" + entity.PersonType, out var personCache))
{
peopleCache[entity.Name] = personCache = (entity, []);
peopleCache[entity.Name + "|" + entity.PersonType] = personCache = (entity, []);
}
if (reader.TryGetString(2, out var role))
@@ -307,6 +352,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
int? listOrder = reader.IsDBNull(5) ? null : reader.GetInt32(5);
personCache.Items.Add(new PeopleBaseItemMap()
{
@@ -314,7 +360,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
ItemId = itemId,
People = null!,
PeopleId = personCache.Person.Id,
ListOrder = sortOrder,
ListOrder = listOrder,
SortOrder = sortOrder,
Role = role
});
@@ -1086,12 +1132,12 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
if (reader.TryGetString(index++, out var providerIds))
{
entity.Provider = providerIds.Split('|').Select(e => e.Split("="))
entity.Provider = providerIds.Split('|').Select(e => e.Split("=")).Where(e => e.Length >= 2)
.Select(e => new BaseItemProvider()
{
Item = null!,
ProviderId = e[0],
ProviderValue = e[1]
ProviderValue = string.Join('|', e.Skip(1))
}).ToArray();
}
@@ -1189,7 +1235,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
ItemId = baseItemId,
Id = Guid.NewGuid(),
Path = e.Path,
Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
Blurhash = e.BlurHash is not null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
DateModified = e.DateModified,
Height = e.Height,
Width = e.Width,

View File

@@ -34,12 +34,8 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
{
if (service.Lifetime == ServiceLifetime.Singleton && !service.ServiceType.IsGenericTypeDefinition)
{
object? serviceInstance = serviceProvider.GetService(service.ServiceType);
if (serviceInstance != null)
{
childServiceCollection.AddSingleton(service.ServiceType, serviceInstance);
continue;
}
childServiceCollection.AddSingleton(service.ServiceType, _ => serviceProvider.GetService(service.ServiceType)!);
continue;
}
childServiceCollection.Add(service);

View File

@@ -98,7 +98,7 @@ public sealed class SetupServer : IDisposable
var maxLevel = logEntry.LogLevel;
var stack = new Stack<StartupLogTopic>(children);
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // error is the highest inherted error level.
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) // error is the highest inherted error level.
{
maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
foreach (var child in logEntry.Children)

View File

@@ -96,6 +96,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.CodeAnalysis", "src\Jellyfin.CodeAnalysis\Jellyfin.CodeAnalysis.csproj", "{11643D0F-6761-4EF7-AB71-6F9F8DE00714}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -258,6 +260,10 @@ Global
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.Build.0 = Release|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -289,6 +295,7 @@ Global
{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
{8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
{11643D0F-6761-4EF7-AB71-6F9F8DE00714} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

View File

@@ -107,8 +107,15 @@ namespace MediaBrowser.Controller.Entities
ProductionLocations = Array.Empty<string>();
RemoteTrailers = Array.Empty<MediaUrl>();
ExtraIds = Array.Empty<Guid>();
UserData = [];
}
/// <summary>
/// Gets or Sets the user data collection as cached from the last Db query.
/// </summary>
[JsonIgnore]
public ICollection<UserData> UserData { get; set; }
[JsonIgnore]
public string PreferredMetadataCountryCode { get; set; }
@@ -701,19 +708,7 @@ namespace MediaBrowser.Controller.Entities
{
get
{
var customRating = CustomRating;
if (!string.IsNullOrEmpty(customRating))
{
return customRating;
}
var parent = DisplayParent;
if (parent is not null)
{
return parent.CustomRatingForComparison;
}
return null;
return GetCustomRatingForComparision();
}
}
@@ -791,6 +786,26 @@ namespace MediaBrowser.Controller.Entities
/// <value>The remote trailers.</value>
public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
{
callstack ??= new();
var customRating = CustomRating;
if (!string.IsNullOrEmpty(customRating))
{
return customRating;
}
callstack.Add(Id);
var parent = DisplayParent;
if (parent is not null && !callstack.Contains(parent.Id))
{
return parent.GetCustomRatingForComparision(callstack);
}
return null;
}
public virtual double GetDefaultPrimaryImageAspectRatio()
{
return 0;
@@ -2307,27 +2322,27 @@ namespace MediaBrowser.Controller.Entities
return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
}
public virtual bool IsPlayed(User user)
public virtual bool IsPlayed(User user, UserItemData userItemData)
{
var userdata = UserDataManager.GetUserData(user, this);
userItemData ??= UserDataManager.GetUserData(user, this);
return userdata is not null && userdata.Played;
return userItemData is not null && userItemData.Played;
}
public bool IsFavoriteOrLiked(User user)
public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
{
var userdata = UserDataManager.GetUserData(user, this);
userItemData ??= UserDataManager.GetUserData(user, this);
return userdata is not null && (userdata.IsFavorite || (userdata.Likes ?? false));
return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
}
public virtual bool IsUnplayed(User user)
public virtual bool IsUnplayed(User user, UserItemData userItemData)
{
ArgumentNullException.ThrowIfNull(user);
var userdata = UserDataManager.GetUserData(user, this);
userItemData ??= UserDataManager.GetUserData(user, this);
return userdata is null || !userdata.Played;
return userItemData is null || !userItemData.Played;
}
ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()

View File

@@ -42,6 +42,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Folder : BaseItem
{
private IEnumerable<BaseItem> _children;
public Folder()
{
LinkedChildren = Array.Empty<LinkedChild>();
@@ -108,11 +110,15 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
/// Gets the actual children.
/// Gets or Sets the actual children.
/// </summary>
/// <value>The actual children.</value>
[JsonIgnore]
public virtual IEnumerable<BaseItem> Children => LoadChildren();
public virtual IEnumerable<BaseItem> Children
{
get => _children ??= LoadChildren();
set => _children = value;
}
/// <summary>
/// Gets thread-safe access to all recursive children of this folder - without regard to user.
@@ -281,6 +287,7 @@ namespace MediaBrowser.Controller.Entities
/// <returns>Task.</returns>
public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default)
{
Children = null; // invalidate cached children.
return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
}
@@ -288,6 +295,7 @@ namespace MediaBrowser.Controller.Entities
{
var dictionary = new Dictionary<Guid, BaseItem>();
Children = null; // invalidate cached children.
var childrenList = Children.ToList();
foreach (var child in childrenList)
@@ -526,6 +534,7 @@ namespace MediaBrowser.Controller.Entities
{
if (validChildrenNeedGeneration)
{
Children = null; // invalidate cached children.
validChildren = Children.ToList();
}
@@ -568,7 +577,8 @@ namespace MediaBrowser.Controller.Entities
if (recursive && child is Folder folder)
{
await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
folder.Children = null; // invalidate cached children.
await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -686,16 +696,22 @@ namespace MediaBrowser.Controller.Entities
IEnumerable<BaseItem> items;
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
var totalCount = 0;
if (query.User is null)
{
items = GetRecursiveChildren(filter);
totalCount = items.Count();
}
else
{
items = GetRecursiveChildren(user, query);
items = GetRecursiveChildren(user, query, out totalCount);
query.Limit = null;
query.StartIndex = null; // override these here as they have already been applied
}
return PostFilterAndSort(items, query);
var result = PostFilterAndSort(items, query);
result.TotalRecordCount = totalCount;
return result;
}
if (this is not UserRootFolder
@@ -944,22 +960,34 @@ namespace MediaBrowser.Controller.Entities
IEnumerable<BaseItem> items;
int totalItemCount = 0;
if (query.User is null)
{
items = Children.Where(filter);
totalItemCount = items.Count();
}
else
{
// need to pass this param to the children.
var childQuery = new InternalItemsQuery
{
DisplayAlbumFolders = query.DisplayAlbumFolders
DisplayAlbumFolders = query.DisplayAlbumFolders,
Limit = query.Limit,
StartIndex = query.StartIndex,
NameStartsWith = query.NameStartsWith,
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
NameLessThan = query.NameLessThan
};
items = GetChildren(user, true, childQuery).Where(filter);
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
query.Limit = null;
query.StartIndex = null;
}
return PostFilterAndSort(items, query);
var result = PostFilterAndSort(items, query);
result.TotalRecordCount = totalItemCount;
return result;
}
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
@@ -1242,30 +1270,30 @@ namespace MediaBrowser.Controller.Entities
return true;
}
public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren)
{
ArgumentNullException.ThrowIfNull(user);
return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
}
public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
{
ArgumentNullException.ThrowIfNull(user);
query ??= new InternalItemsQuery();
query.User = user;
// the true root should return our users root folder children
if (IsPhysicalRoot)
{
return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
}
var result = new Dictionary<Guid, BaseItem>();
AddChildren(user, includeLinkedChildren, result, false, query);
totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
return result.Values.ToArray();
}
public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query = null)
{
return GetChildren(user, includeLinkedChildren, out _, query);
}
protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return Children;
@@ -1274,13 +1302,13 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the children to list.
/// </summary>
private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
{
// Prevent infinite recursion of nested folders
visitedFolders ??= new HashSet<Folder>();
if (!visitedFolders.Add(this))
{
return;
return 0;
}
// If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
@@ -1297,44 +1325,59 @@ namespace MediaBrowser.Controller.Entities
children = GetEligibleChildrenForRecursiveChildren(user);
}
AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
if (includeLinkedChildren)
{
AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders);
children = children.Concat(GetLinkedChildren(user)).ToArray();
}
return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
}
private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
{
foreach (var child in children)
{
if (!child.IsVisible(user))
{
continue;
}
query ??= new InternalItemsQuery();
var limit = query.Limit > 0 ? query.Limit : int.MaxValue;
query.Limit = 0;
if (query is null || UserViewBuilder.FilterItem(child, query))
var visibleChildren = children
.Where(e => e.IsVisible(user))
.ToArray();
var realChildren = visibleChildren
.Where(e => query is null || UserViewBuilder.FilterItem(e, query))
.ToArray();
var childCount = realChildren.Length;
if (result.Count < limit)
{
var remainingCount = (int)(limit - result.Count);
foreach (var child in realChildren
.Skip(query.StartIndex ?? 0)
.Take(remainingCount))
{
result[child.Id] = child;
}
}
if (recursive && child.IsFolder)
if (recursive)
{
foreach (var child in visibleChildren
.Where(e => e.IsFolder)
.OfType<Folder>())
{
var folder = (Folder)child;
folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
}
}
return childCount;
}
public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
ArgumentNullException.ThrowIfNull(user);
var result = new Dictionary<Guid, BaseItem>();
AddChildren(user, true, result, true, query);
totalCount = AddChildren(user, true, result, true, query);
return result.Values.ToArray();
}
@@ -1666,23 +1709,14 @@ namespace MediaBrowser.Controller.Entities
}
}
public override bool IsPlayed(User user)
public override bool IsPlayed(User user, UserItemData userItemData)
{
var itemsResult = GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
IsFolder = false,
IsVirtualItem = false,
EnableTotalRecordCount = false
});
return itemsResult
.All(i => i.IsPlayed(user));
return ItemRepository.GetIsPlayed(user, Id, true);
}
public override bool IsUnplayed(User user)
public override bool IsUnplayed(User user, UserItemData userItemData)
{
return !IsPlayed(user);
return !IsPlayed(user, userItemData);
}
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)

View File

@@ -136,9 +136,9 @@ namespace MediaBrowser.Controller.Entities.Movies
return Sort(children, user).ToArray();
}
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
var children = base.GetRecursiveChildren(user, query);
var children = base.GetRecursiveChildren(user, query, out totalCount);
return Sort(children, user).ToArray();
}

View File

@@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Entities.TV
public override int GetChildCount(User user)
{
var result = GetChildren(user, true).Count;
var result = GetChildren(user, true, null).Count;
return result;
}

View File

@@ -297,6 +297,7 @@ namespace MediaBrowser.Controller.Entities.TV
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
Children = null; // invalidate cached children.
// Refresh bottom up, seasons and episodes first, then the series
var items = GetRecursiveChildren();

View File

@@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.Entities
/// <inheritdoc />
public override int GetChildCount(User user)
{
return GetChildren(user, true).Count;
return GetChildren(user, true, null).Count;
}
/// <inheritdoc />
@@ -134,20 +134,22 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
query.SetUser(user);
query.Recursive = true;
query.EnableTotalRecordCount = false;
query.ForceDirect = true;
var data = GetItemList(query);
totalCount = data.Count;
return GetItemList(query);
return data;
}
/// <inheritdoc />
protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return GetChildren(user, false);
return GetChildren(user, false, null);
}
public static bool IsUserSpecific(Folder folder)

View File

@@ -472,6 +472,23 @@ namespace MediaBrowser.Controller.Entities
public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager)
{
if (!string.IsNullOrEmpty(query.NameStartsWith) && !item.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
#pragma warning disable CA1309 // Use ordinal string comparison
if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater) && string.Compare(query.NameStartsWithOrGreater, item.SortName, StringComparison.InvariantCultureIgnoreCase) == 1)
{
return false;
}
if (!string.IsNullOrEmpty(query.NameLessThan) && string.Compare(query.NameLessThan, item.SortName, StringComparison.InvariantCultureIgnoreCase) != 1)
#pragma warning restore CA1309 // Use ordinal string comparison
{
return false;
}
if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType))
{
return false;
@@ -502,7 +519,6 @@ namespace MediaBrowser.Controller.Entities
if (query.IsLiked.HasValue)
{
userData = userDataManager.GetUserData(user, item);
if (!userData.Likes.HasValue || userData.Likes != query.IsLiked.Value)
{
return false;
@@ -511,7 +527,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsFavoriteOrLiked.HasValue)
{
userData = userData ?? userDataManager.GetUserData(user, item);
userData ??= userDataManager.GetUserData(user, item);
var isFavoriteOrLiked = userData.IsFavorite || (userData.Likes ?? false);
if (isFavoriteOrLiked != query.IsFavoriteOrLiked.Value)
@@ -522,8 +538,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsFavorite.HasValue)
{
userData = userData ?? userDataManager.GetUserData(user, item);
userData ??= userDataManager.GetUserData(user, item);
if (userData.IsFavorite != query.IsFavorite.Value)
{
return false;
@@ -532,7 +547,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsResumable.HasValue)
{
userData = userData ?? userDataManager.GetUserData(user, item);
userData ??= userDataManager.GetUserData(user, item);
var isResumable = userData.PlaybackPositionTicks > 0;
if (isResumable != query.IsResumable.Value)
@@ -543,7 +558,8 @@ namespace MediaBrowser.Controller.Entities
if (query.IsPlayed.HasValue)
{
if (item.IsPlayed(user) != query.IsPlayed.Value)
userData ??= userDataManager.GetUserData(user, item);
if (item.IsPlayed(user, userData) != query.IsPlayed.Value)
{
return false;
}

View File

@@ -152,16 +152,7 @@ namespace MediaBrowser.Controller.Entities
{
get
{
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
var item = LibraryManager.GetItemById(PrimaryVersionId);
if (item is Video video)
{
return video.MediaSourceCount;
}
}
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
return GetMediaSourceCount();
}
}
@@ -259,6 +250,27 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override MediaType MediaType => MediaType.Video;
private int GetMediaSourceCount(HashSet<Guid> callstack = null)
{
callstack ??= new();
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
var item = LibraryManager.GetItemById(PrimaryVersionId);
if (item is Video video)
{
if (callstack.Contains(video.Id))
{
return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
}
callstack.Add(video.Id);
return video.GetMediaSourceCount(callstack);
}
}
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
}
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();

View File

@@ -167,12 +167,12 @@ public static class XmlReaderExtensions
// Only split by comma if there is no pipe in the string
// We have to be careful to not split names like Matthew, Jr.
var separator = !value.Contains('|', StringComparison.Ordinal)
ReadOnlySpan<char> separator = !value.Contains('|', StringComparison.Ordinal)
&& !value.Contains(';', StringComparison.Ordinal)
? new[] { ',' }
: new[] { '|', ';' };
? stackalloc[] { ',' }
: stackalloc[] { '|', ';' };
foreach (var part in value.Trim().Trim(separator).Split(separator))
foreach (var part in value.AsSpan().Trim().Trim(separator).ToString().Split(separator))
{
if (!string.IsNullOrWhiteSpace(part))
{

View File

@@ -60,8 +60,15 @@ namespace MediaBrowser.Controller
void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences);
/// <summary>
/// Saves changes made to the database.
/// Updates or Creates the display preferences.
/// </summary>
void SaveChanges();
/// <param name="displayPreferences">The entity to update or create.</param>
void UpdateDisplayPreferences(DisplayPreferences displayPreferences);
/// <summary>
/// Updates or Creates the display preferences for the given item.
/// </summary>
/// <param name="itemDisplayPreferences">The entity to update or create.</param>
void UpdateItemDisplayPreferences(ItemDisplayPreferences itemDisplayPreferences);
}
}

View File

@@ -336,6 +336,13 @@ namespace MediaBrowser.Controller.Library
/// <param name="options">Options to use for deletion.</param>
void DeleteItem(BaseItem item, DeleteOptions options);
/// <summary>
/// Deletes items that are not having any children like Actors.
/// </summary>
/// <param name="items">Items to delete.</param>
/// <remarks>In comparison to <see cref="DeleteItem(BaseItem, DeleteOptions, BaseItem, bool)"/> this method skips a lot of steps assuming there are no children to recusively delete nor does it define the special handling for channels and alike.</remarks>
public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items);
/// <summary>
/// Deletes the item.
/// </summary>
@@ -624,6 +631,8 @@ namespace MediaBrowser.Controller.Library
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query);
IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names);
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query);
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query);

View File

@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Enums;
@@ -22,8 +21,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// For now, a common base class until the API and MediaEncoding classes are unified
public class EncodingJobInfo
{
public int? OutputAudioBitrate;
public int? OutputAudioChannels;
private static readonly char[] _separators = ['|', ','];
private TranscodeReason? _transcodeReasons = null;
@@ -36,6 +34,10 @@ namespace MediaBrowser.Controller.MediaEncoding
SupportedSubtitleCodecs = Array.Empty<string>();
}
public int? OutputAudioBitrate { get; set; }
public int? OutputAudioChannels { get; set; }
public TranscodeReason TranscodeReasons
{
get
@@ -586,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(BaseRequest.Profile))
{
return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codec))
@@ -595,7 +597,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(profile))
{
return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}
@@ -606,7 +608,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
{
return BaseRequest.VideoRangeType.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codec))
@@ -615,7 +617,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(rangetype))
{
return rangetype.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}
@@ -626,7 +628,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
{
return BaseRequest.CodecTag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codec))
@@ -635,7 +637,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codectag))
{
return codectag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}

View File

@@ -53,5 +53,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>System.String.</returns>
Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
/// <summary>
/// Extracts all extractable subtitles (text and pgs).
/// </summary>
/// <param name="mediaSource">The mediaSource.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken);
}
}

View File

@@ -80,6 +80,16 @@ namespace MediaBrowser.Controller.Net
/// <returns>Task{`1}.</returns>
protected abstract Task<TReturnDataType> GetDataToSend();
/// <summary>
/// Gets the data to send for a specific connection.
/// </summary>
/// <param name="connection">The connection.</param>
/// <returns>Task{`1}.</returns>
protected virtual Task<TReturnDataType> GetDataToSendForConnection(IWebSocketConnection connection)
{
return GetDataToSend();
}
/// <summary>
/// Processes the message.
/// </summary>
@@ -174,17 +184,11 @@ namespace MediaBrowser.Controller.Net
continue;
}
var data = await GetDataToSend().ConfigureAwait(false);
if (data is null)
{
continue;
}
IEnumerable<Task> GetTasks()
{
foreach (var tuple in tuples)
{
yield return SendDataInternal(data, tuple);
yield return SendDataForConnectionAsync(tuple);
}
}
@@ -198,12 +202,19 @@ namespace MediaBrowser.Controller.Net
}
}
private async Task SendDataInternal(TReturnDataType data, (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple)
private async Task SendDataForConnectionAsync((IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple)
{
try
{
var (connection, cts, state) = tuple;
var cancellationToken = cts.Token;
var data = await GetDataToSendForConnection(connection).ConfigureAwait(false);
if (data is null)
{
return;
}
await connection.SendAsync(
new OutboundWebSocketMessage<TReturnDataType> { MessageType = Type, Data = data },
cancellationToken).ConfigureAwait(false);

View File

@@ -7,7 +7,9 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
@@ -21,8 +23,8 @@ public interface IItemRepository
/// <summary>
/// Deletes the item.
/// </summary>
/// <param name="id">The identifier.</param>
void DeleteItem(Guid id);
/// <param name="ids">The identifier to delete.</param>
void DeleteItem(params IReadOnlyList<Guid> ids);
/// <summary>
/// Saves the items.
@@ -112,4 +114,20 @@ public interface IItemRepository
/// <param name="id">The id to check.</param>
/// <returns>True if the item exists, otherwise false.</returns>
Task<bool> ItemExistsAsync(Guid id);
/// <summary>
/// Gets a value indicating wherever all children of the requested Id has been played.
/// </summary>
/// <param name="user">The userdata to check against.</param>
/// <param name="id">The Top id to check.</param>
/// <param name="recursive">Whever the check should be done recursive. Warning expensive operation.</param>
/// <returns>A value indicating whever all children has been played.</returns>
bool GetIsPlayed(User user, Guid id, bool recursive);
/// <summary>
/// Gets all artist matches from the db.
/// </summary>
/// <param name="artistNames">The names of the artists.</param>
/// <returns>A map of the artist name and the potential matches.</returns>
IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames);
}

View File

@@ -149,9 +149,11 @@ namespace MediaBrowser.Controller.Playlists
return [];
}
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
return GetPlayableItems(user, query);
var items = GetPlayableItems(user, query);
totalCount = items.Count;
return items;
}
public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetManageableItems()

View File

@@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
if (_proberSupportsFirstVideoFrame)
if (!isAudio && _proberSupportsFirstVideoFrame)
{
args += " -show_frames -only_first_vframe";
}
@@ -1122,6 +1122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private void StartProcess(ProcessWrapper process)
{
process.Process.Start();
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
lock (_runningProcessesLock)
{

View File

@@ -30,9 +30,11 @@ namespace MediaBrowser.MediaEncoding.Probing
private const string ArtistReplaceValue = " | ";
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
private readonly string[] _webmVideoCodecs = { "av1", "vp8", "vp9" };
private readonly string[] _webmAudioCodecs = { "opus", "vorbis" };
private static readonly char[] _basicDelimiters = ['/', ';'];
private static readonly char[] _nameDelimiters = [.. _basicDelimiters, '|', '\\'];
private static readonly char[] _genreDelimiters = [.. _basicDelimiters, ','];
private static readonly string[] _webmVideoCodecs = ["av1", "vp8", "vp9"];
private static readonly string[] _webmAudioCodecs = ["opus", "vorbis"];
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
@@ -174,7 +176,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (tags.TryGetValue("artists", out var artists) && !string.IsNullOrWhiteSpace(artists))
{
info.Artists = SplitDistinctArtists(artists, new[] { '/', ';' }, false).ToArray();
info.Artists = SplitDistinctArtists(artists, _basicDelimiters, false).ToArray();
}
else
{
@@ -309,7 +311,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return string.Join(',', splitFormat.Where(s => !string.IsNullOrEmpty(s)));
}
private int? GetEstimatedAudioBitrate(string codec, int? channels)
private static int? GetEstimatedAudioBitrate(string codec, int? channels)
{
if (!channels.HasValue)
{
@@ -530,7 +532,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return pairs;
}
private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
private static void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
{
List<BaseItemPerson> peoples = new List<BaseItemPerson>();
var distinctPairs = pairs.Select(p => p.Value)
@@ -579,7 +581,7 @@ namespace MediaBrowser.MediaEncoding.Probing
info.People = peoples.ToArray();
}
private NameValuePair GetNameValuePair(XmlReader reader)
private static NameValuePair GetNameValuePair(XmlReader reader)
{
string name = null;
string value = null;
@@ -624,7 +626,7 @@ namespace MediaBrowser.MediaEncoding.Probing
};
}
private string NormalizeSubtitleCodec(string codec)
private static string NormalizeSubtitleCodec(string codec)
{
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
{
@@ -850,12 +852,36 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
// stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
stream.IsAnamorphic = string.Equals(streamInfo.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase);
if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
{
stream.IsAnamorphic = false;
}
else if (!string.Equals(streamInfo.SampleAspectRatio, "0:1", StringComparison.Ordinal))
{
stream.IsAnamorphic = true;
}
else if (string.Equals(streamInfo.DisplayAspectRatio, "0:1", StringComparison.Ordinal))
{
stream.IsAnamorphic = false;
}
else if (!string.Equals(
streamInfo.DisplayAspectRatio,
// Force GetAspectRatio() to derive ratio from Width/Height directly by using null DAR
GetAspectRatio(new MediaStreamInfo
{
Width = streamInfo.Width,
Height = streamInfo.Height,
DisplayAspectRatio = null
}),
StringComparison.Ordinal))
{
stream.IsAnamorphic = true;
}
else
{
stream.IsAnamorphic = false;
}
if (streamInfo.Refs > 0)
{
@@ -908,12 +934,10 @@ namespace MediaBrowser.MediaEncoding.Probing
}
var frameInfo = frameInfoList?.FirstOrDefault(i => i.StreamIndex == stream.Index);
if (frameInfo?.SideDataList != null)
if (frameInfo?.SideDataList is not null
&& frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase)))
{
if (frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase)))
{
stream.Hdr10PlusPresentFlag = true;
}
stream.Hdr10PlusPresentFlag = true;
}
}
else if (streamInfo.CodecType == CodecType.Data)
@@ -1000,7 +1024,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return stream;
}
private void NormalizeStreamTitle(MediaStream stream)
private static void NormalizeStreamTitle(MediaStream stream)
{
if (string.Equals(stream.Title, "cc", StringComparison.OrdinalIgnoreCase)
|| stream.Type == MediaStreamType.EmbeddedImage)
@@ -1015,7 +1039,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.String.</returns>
private string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
private static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
{
if (tags is null)
{
@@ -1027,7 +1051,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return val;
}
private string ParseChannelLayout(string input)
private static string ParseChannelLayout(string input)
{
if (string.IsNullOrEmpty(input))
{
@@ -1037,7 +1061,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return input.AsSpan().LeftPart('(').ToString();
}
private string GetAspectRatio(MediaStreamInfo info)
private static string GetAspectRatio(MediaStreamInfo info)
{
var original = info.DisplayAspectRatio;
@@ -1106,7 +1130,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return original;
}
private bool IsClose(double d1, double d2, double variance = .005)
private static bool IsClose(double d1, double d2, double variance = .005)
{
return Math.Abs(d1 - d2) <= variance;
}
@@ -1139,7 +1163,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return divisor == 0f ? null : dividend / divisor;
}
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
private static void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
{
// Get the first info stream
var stream = result.Streams?.FirstOrDefault(s => s.CodecType == CodecType.Audio);
@@ -1164,7 +1188,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
private int? GetBPSFromTags(MediaStreamInfo streamInfo)
private static int? GetBPSFromTags(MediaStreamInfo streamInfo)
{
if (streamInfo?.Tags is null)
{
@@ -1180,7 +1204,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return null;
}
private double? GetRuntimeSecondsFromTags(MediaStreamInfo streamInfo)
private static double? GetRuntimeSecondsFromTags(MediaStreamInfo streamInfo)
{
if (streamInfo?.Tags is null)
{
@@ -1196,7 +1220,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return null;
}
private long? GetNumberOfBytesFromTags(MediaStreamInfo streamInfo)
private static long? GetNumberOfBytesFromTags(MediaStreamInfo streamInfo)
{
if (streamInfo?.Tags is null)
{
@@ -1213,7 +1237,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return null;
}
private void SetSize(InternalMediaInfoResult data, MediaInfo info)
private static void SetSize(InternalMediaInfoResult data, MediaInfo info)
{
if (data.Format is null)
{
@@ -1358,7 +1382,7 @@ namespace MediaBrowser.MediaEncoding.Probing
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, mb);
}
private string GetMultipleMusicBrainzId(string value)
private static string GetMultipleMusicBrainzId(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
@@ -1530,7 +1554,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (tags.TryGetValue("WM/Genre", out var genres) && !string.IsNullOrWhiteSpace(genres))
{
var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var genreList = genres.Split(_genreDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// If this is empty then don't overwrite genres that might have been fetched earlier
if (genreList.Length > 0)
@@ -1547,7 +1571,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (tags.TryGetValue("WM/MediaCredits", out var people) && !string.IsNullOrEmpty(people))
{
video.People = Array.ConvertAll(
people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
people.Split(_basicDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
i => new BaseItemPerson { Name = i, Type = PersonKind.Actor });
}
@@ -1656,7 +1680,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
// REVIEW: find out why the byte array needs to be 197 bytes long and comment the reason
private TransportStreamTimestamp GetMpegTimestamp(string path)
private static TransportStreamTimestamp GetMpegTimestamp(string path)
{
var packetBuffer = new byte[197];

View File

@@ -169,7 +169,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
if (fileInfo.IsExternal)
{
using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false))
var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
var detected = result.Detected;
@@ -476,13 +477,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|| string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Extracts all extractable subtitles (text and pgs).
/// </summary>
/// <param name="mediaSource">The mediaSource.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken)
/// <inheritdoc />
public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken)
{
var locks = new List<IDisposable>();
var extractableStreams = new List<MediaStream>();
@@ -937,7 +933,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
.ConfigureAwait(false);
}
using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false))
var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
var charset = result.Detected?.EncodingName ?? string.Empty;

View File

@@ -69,14 +69,8 @@ public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo>
if (mergeMetadataSettings)
{
if (replaceData || targetItem.LinkedChildren.Length == 0)
{
targetItem.LinkedChildren = sourceItem.LinkedChildren;
}
else
{
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
}
// TODO: Change to only replace when currently empty or requested. This is currently not done because the metadata service is not handling attaching collection items based on the provider responses
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray();
}
}

View File

@@ -1279,7 +1279,7 @@ namespace MediaBrowser.Providers.Manager
{
if (source is Video sourceCast && target is Video targetCast)
{
if (replaceData || !targetCast.Video3DFormat.HasValue)
if (sourceCast.Video3DFormat.HasValue && (replaceData || !targetCast.Video3DFormat.HasValue))
{
targetCast.Video3DFormat = sourceCast.Video3DFormat;
}

View File

@@ -437,12 +437,12 @@ namespace MediaBrowser.Providers.MediaInfo
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, recordingMbId);
}
else if (TryGetSanitizedAdditionalFields(track, "UFID", out var ufIdValue) && !string.IsNullOrEmpty(ufIdValue))
else if (TryGetSanitizedUFIDFields(track, out var owner, out var identifier) && !string.IsNullOrEmpty(owner) && !string.IsNullOrEmpty(identifier))
{
// If tagged with MB Picard, the format is 'http://musicbrainz.org\0<recording MBID>'
if (ufIdValue.Contains("musicbrainz.org", StringComparison.OrdinalIgnoreCase))
if (owner.Contains("musicbrainz.org", StringComparison.OrdinalIgnoreCase))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, ufIdValue.AsSpan().RightPart('\0').ToString());
audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, identifier);
}
}
}
@@ -537,5 +537,24 @@ namespace MediaBrowser.Providers.MediaInfo
value = GetSanitizedStringTag(value, track.Path);
return hasField;
}
private bool TryGetSanitizedUFIDFields(Track track, out string? owner, out string? identifier)
{
var hasField = track.AdditionalFields.TryGetValue("UFID", out string? value);
if (hasField && !string.IsNullOrEmpty(value))
{
string[] parts = value.Split('\0');
if (parts.Length == 2)
{
owner = GetSanitizedStringTag(parts[0], track.Path);
identifier = GetSanitizedStringTag(parts[1], track.Path);
return true;
}
}
owner = null;
identifier = null;
return false;
}
}
}

View File

@@ -130,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
if (file is not null && item.HasChanged(file.LastWriteTimeUtc) && file.Length != item.Size)
if (file is not null && item.HasChanged(file.LastWriteTimeUtc))
{
_logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path);
return true;

View File

@@ -109,14 +109,14 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
var albumArtists = songs
.SelectMany(i => i.AlbumArtists)
.GroupBy(i => i)
.GroupBy(i => i, StringComparer.OrdinalIgnoreCase)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToArray();
updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbumArtist);
if (!item.AlbumArtists.SequenceEqual(albumArtists, StringComparer.OrdinalIgnoreCase))
if (!item.AlbumArtists.SequenceEqual(albumArtists, StringComparer.Ordinal))
{
item.AlbumArtists = albumArtists;
updateType |= ItemUpdateType.MetadataEdit;
@@ -131,12 +131,12 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
var artists = songs
.SelectMany(i => i.Artists)
.GroupBy(i => i)
.GroupBy(i => i, StringComparer.OrdinalIgnoreCase)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToArray();
if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
if (!item.Artists.SequenceEqual(artists, StringComparer.Ordinal))
{
item.Artists = artists;
updateType |= ItemUpdateType.MetadataEdit;

View File

@@ -213,15 +213,18 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList();
var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
if (ourRelease is not null)
{
movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification);
}
else if (usRelease is not null)
else
{
movie.OfficialRating = usRelease.Certification;
var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
if (usRelease is not null)
{
movie.OfficialRating = usRelease.Certification;
}
}
}
@@ -340,9 +343,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieResult.Videos?.Results is not null)
{
var trailers = new List<MediaUrl>();
for (var i = 0; i < movieResult.Videos.Results.Count; i++)
var sortedVideos = movieResult.Videos.Results
.OrderByDescending(video => string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase));
foreach (var video in sortedVideos)
{
var video = movieResult.Videos.Results[i];
if (!TmdbUtils.IsTrailerType(video))
{
continue;

View File

@@ -39,7 +39,9 @@
---
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET platform to enable full cross-platform support.
There are no strings attached, no premium licenses or features, and no hidden agendas: just a team that wants to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
For further details, please see [our documentation page](https://jellyfin.org/docs/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://jellyfin.org/docs/general/getting-help). For more information about the project, please see our [about page](https://jellyfin.org/docs/general/about).

View File

@@ -0,0 +1,9 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
## Release 1.0
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
JF0001 | Usage | Warning | Async-created IAsyncDisposable objects should use 'await using'

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Jellyfin.CodeAnalysis;
/// <summary>
/// Analyzer to detect sync disposal of async-created IAsyncDisposable objects.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsyncDisposalPatternAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// Diagnostic descriptor for sync disposal of async-created IAsyncDisposable objects.
/// </summary>
public static readonly DiagnosticDescriptor AsyncDisposableSyncDisposal = new(
id: "JF0001",
title: "Async-created IAsyncDisposable objects should use 'await using'",
messageFormat: "Using 'using' with async-created IAsyncDisposable object '{0}'. Use 'await using' instead to prevent resource leaks.",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Objects that implement IAsyncDisposable and are created using 'await' should be disposed using 'await using' to prevent resource leaks.");
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [AsyncDisposableSyncDisposal];
/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeUsingStatement, SyntaxKind.UsingStatement);
}
private static void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context)
{
var usingStatement = (UsingStatementSyntax)context.Node;
// Skip 'await using' statements
if (usingStatement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
{
return;
}
// Check if there's a variable declaration
if (usingStatement.Declaration?.Variables is null)
{
return;
}
foreach (var variable in usingStatement.Declaration.Variables)
{
if (variable.Initializer?.Value is AwaitExpressionSyntax awaitExpression)
{
var typeInfo = context.SemanticModel.GetTypeInfo(awaitExpression);
var type = typeInfo.Type;
if (type is not null && ImplementsIAsyncDisposable(type))
{
var diagnostic = Diagnostic.Create(
AsyncDisposableSyncDisposal,
usingStatement.GetLocation(),
type.Name);
context.ReportDiagnostic(diagnostic);
}
}
}
}
private static bool ImplementsIAsyncDisposable(ITypeSymbol type)
{
return type.AllInterfaces.Any(i =>
string.Equals(i.Name, "IAsyncDisposable", StringComparison.Ordinal)
&& string.Equals(i.ContainingNamespace?.ToDisplayString(), "System", StringComparison.Ordinal));
}
}

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