Compare commits

..

102 Commits

Author SHA1 Message Date
renovate[bot]
3f678cf9f3 Update dependency Microsoft.NET.Test.Sdk to 18.4.0 2026-04-07 11:01:37 +00:00
Lofuuzi
b28a5794ec Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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/zh_Hant_HK/
2026-04-06 19:36:57 +00:00
Joshua M. Boniface
179db631f7 Merge pull request #16577 from Bond-009/security-backports
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (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
Backport security fixes
2026-04-06 09:37:59 -04:00
Shadowghost
740e9f8749 Lock down tuner API to be admin-only 2026-04-06 11:37:45 +02:00
Shadowghost
0bf7653e36 Fix GHSA-jh22-fw8w-2v9x 2026-04-06 11:37:45 +02:00
Shadowghost
b846958f2c Add additional validations 2026-04-06 11:37:45 +02:00
Shadowghost
3c2833e3e8 Fix GHSA v2jv-54xj-h76w 2026-04-06 11:37:45 +02:00
Joshua M. Boniface
3c9b71e124 Fix GHSA-8fw7-f233-ffr8 with improved sanitization
Co-Authored-By: Shadowghost <Ghost_of_Stone@web.de>
2026-04-06 11:37:45 +02:00
Joshua M. Boniface
8cecf53057 Fix GHSA-j2hf-x4q5-47j3 with improved sanitization
Co-Authored-By: Shadowghost <Ghost_of_Stone@web.de>
2026-04-06 11:37:45 +02:00
Shadowghost
c008f28d31 Backport pull request #16540 from jellyfin/release-10.11.z
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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
Handle folders without associated library in FixLibrarySubtitleDownloadLanguages

Original-merge: be095f85ab

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-04-06 05:19:33 -04:00
MBR-0001
142ba42883 Backport pull request #16539 from jellyfin/release-10.11.z
Fix subtitle saving

Original-merge: f51c63e244

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-04-06 05:19:32 -04:00
Bond-009
cf9b8161ef Merge pull request #16342 from Bond-009/spammydebug
Don't spam debug log with items without rating
2026-04-06 10:52:17 +02:00
kscop-n1
83c9ab0079 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/
2026-04-06 08:50:12 +00:00
kscop-n1
f5e9c1de45 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/
2026-04-06 08:50:00 +00:00
Bas
76c17856ba Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2026-04-06 08:50:00 +00:00
Bill Thornton
4b59d8f117 Merge pull request #16573 from theguymadmax/update-template-for-10.11.8 2026-04-05 21:22:47 -04:00
Lofuuzi
31720cef05 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / 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
OpenAPI Publish / OpenAPI - Publish Artifact (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/zh_Hant_HK/
2026-04-05 22:08:36 +00:00
theguymadmax
34e2f91d50 Update issue template version to 10.11.8 2026-04-05 16:35:15 -04:00
Bond-009
42a0892022 Merge pull request #16490 from theguymadmax/fix-nested-boxsets-query
Some checks failed
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Fix BoxSet parentId being ignored in item queries
2026-04-05 16:53:44 +02:00
Bond-009
95562f9fee Merge pull request #16501 from bilbofroggins/master
Reverse check for track changed
2026-04-05 16:53:22 +02:00
Bond-009
643116e1d0 Merge pull request #16570 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.12.0
2026-04-05 16:52:23 +02:00
Bond-009
9bf04df5d9 Merge pull request #14745 from HeroBrine1st/master
Add AlbumNormalizationGain field to BaseItemDto
2026-04-05 16:38:35 +02:00
renovate[bot]
8482b3cfb9 Update dependency z440.atl.core to 7.12.0 2026-04-05 14:03:00 +00:00
Weblate
7f3e27c007 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/
2026-04-05 09:36:44 +00:00
Lofuuzi
3225023c1f Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2026-04-05 09:36:41 +00:00
lednurb
cc7bfff412 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2026-04-05 09:36:41 +00:00
Bond-009
70a48e1da4 Merge pull request #16397 from dkanada/person-queries
add StartIndex and ParentId to person search
2026-04-05 11:33:39 +02:00
dkanada
80df5dc984 add StartIndex and ParentId to person search 2026-04-05 15:40:18 +09:00
Niels van Velzen
982d7432f9 Merge pull request #16569 from dkanada/sed-arguments
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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
edit openapi files in place with sed
2026-04-05 08:39:31 +02:00
dkanada
97a1feb16d edit openapi files in place with sed 2026-04-05 15:19:10 +09:00
Bond-009
894768c4f9 Merge pull request #16561 from dkanada/workflow-spec
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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
fix invalid workflow on openapi report job
2026-04-04 18:37:58 +02:00
Kisnov
134fe92f42 Translated using Weblate (Catalan)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2026-04-04 13:19:32 +00:00
dkanada
5d2a529fb3 fix invalid workflow on openapi report job 2026-04-04 17:39:15 +09:00
lednurb
94b3d41d7d Translated using Weblate (Dutch)
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (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/nl/
2026-04-04 07:07:56 +00:00
Joshua M. Boniface
a2dcaa9521 Merge pull request #15902 from ZeusCraft10/fix/udp-discovery-cross-subnet-ipv6 2026-04-04 01:53:59 -04:00
Joshua M. Boniface
e4d6b8e1bd Merge pull request #16553 from theguymadmax/fix-blocked-music 2026-04-04 01:53:18 -04:00
Joshua M. Boniface
7256ac81c0 Merge pull request #16556 from dkanada/openapi-report 2026-04-04 01:52:06 -04:00
Lofuuzi
6e81226054 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
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
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2026-04-03 23:42:40 +00:00
Lofuuzi
12af9f9a57 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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/zh_Hant_HK/
2026-04-03 13:42:42 +00:00
MrPlow
83ee080200 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2026-04-03 13:42:41 +00:00
dkanada
da4d06c5ab move permissions block to publish job 2026-04-03 20:17:31 +09:00
dkanada
87c8349c6b fix openapi report and publish workflows 2026-04-03 19:52:37 +09:00
theguymadmax
c5726559fd Fix parental ratings not working on music albums 2026-04-02 19:43:53 -04:00
Niels van Velzen
9e489cd41f Merge pull request #16419 from Shadowghost/extend-segment-interface
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Add callback for segment data pruning to IMediaSegmentProvider
2026-04-02 22:00:01 +02:00
HeroBrine1st Erquilenne
9b00854e68 Add AlbumNormalizationGain field to BaseItemDto 2026-04-02 20:03:48 +03:00
rimasx
397147d035 Translated using Weblate (Estonian)
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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/et/
2026-04-02 12:28:46 +00:00
Tushar Mhatre
f788e8b741 Translated using Weblate (Hindi)
Some checks failed
CodeQL / Analyze (csharp) (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 Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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/hi/
2026-04-02 07:01:38 +00:00
g10rga321
7c57b62ece Translated using Weblate (Georgian)
Some checks failed
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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/ka/
2026-04-01 09:14:58 +00:00
Niels van Velzen
c7ac591548 Merge pull request #16530 from theguymadmax/update-template-for-10.11.7
Update issue template version to 10.11.7
2026-04-01 09:26:00 +02:00
theguymadmax
736a01f447 Update issue template version to 10.11.7 2026-03-31 22:05:37 -04:00
Lofuuzi
cef81ae2ed Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (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/zh_Hant_HK/
2026-03-31 16:00:40 +00:00
Bond-009
376769c732 Merge pull request #16524 from dkanada/api-workflows
remove nested directory for openapi workflows
2026-03-31 17:49:59 +02:00
Lofuuzi
b6e4b3a4f5 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2026-03-30 18:45:18 +00:00
Bond-009
d5d4309417 Backport pull request #16522 from jellyfin/release-10.11.z
Fix CA1810 build error

Original-merge: 7e88b18192

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-30 14:08:09 -04:00
nyanmisaka
5b3537b3d7 Backport pull request #16519 from jellyfin/release-10.11.z
Fix Null was not checked before using the H264 profile

Original-merge: 89e914c7f1

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-30 14:08:07 -04:00
theguymadmax
2134ea3f7f Backport pull request #16514 from jellyfin/release-10.11.z
Fix lint issue

Original-merge: e1691e649e

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-30 14:08:06 -04:00
Molier
42e8a780ca Backport pull request #16440 from jellyfin/release-10.11.z
Remove -copyts and add -flush_packets 1 to subtitle extraction

Original-merge: ec33c74ec4

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-30 14:08:05 -04:00
theguymadmax
7825fa4e43 Backport pull request #16425 from jellyfin/release-10.11.z
Fix restore backup metadata location

Original-merge: 0f1732e5f5

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-30 14:08:04 -04:00
dkanada
c5ee639016 remove nested directory for openapi workflows 2026-03-31 01:41:47 +09:00
Bond-009
e4f01973b7 Merge pull request #16365 from Shadowghost/cleanup-trickplay-failure
Cleanup trickplay cache dir on failure
2026-03-30 18:39:37 +02:00
Bond-009
5f30a4538b Merge pull request #16507 from Bond-009/isavc
Only set IsAvc for video streams
2026-03-30 18:34:55 +02:00
Niels van Velzen
a536ef88c4 Merge pull request #16463 from dkanada/pull-request
Some checks failed
CodeQL / Analyze (csharp) (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
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
split openapi workflows between pull request and merge
2026-03-30 09:41:01 +02:00
krvi
a0834973ed Translated using Weblate (Faroese)
Some checks failed
CodeQL / Analyze (csharp) (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
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
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-03-29 22:35:24 +00:00
Shadowghost
acaeba11f3 Apply review comments 2026-03-30 00:12:35 +02:00
Bond-009
a3960b30c0 Backport pull request #16369 from jellyfin/release-10.11.z
Some checks failed
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
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
CodeQL / Analyze (csharp) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Fix nullref ex in font handling

Original-merge: 41c2d51d8c

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-29 14:28:41 -04:00
Bond_009
a6da575785 Only set IsAvc for video streams
Also enables nullable for MediaStreamInfo
Makes more properties nullable that aren't always present
2026-03-29 14:27:49 +02:00
upscaylman
ea206f43a2 recognize underscore and dot separators for multi-version grouping (#16465)
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
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
* Add underscore and dot as multi-version file separators

Extend IsEligibleForMultiVersion to recognize _ and . as valid
separators between the base movie name and the version suffix.

Common naming patterns like 'Movie_4K.mkv' or 'Movie.UHD.mkv'
are now correctly grouped as alternate versions during library scan.

* Address review: remove comment, add 3D recognition assertions

---------

Co-authored-by: aimarshall615-creator <aimarshall615@gmail.com>
2026-03-29 12:42:36 +02:00
scheilch
5cfa466d8b fix: cap GetVideoBitrateParamValue at 400 Mbps (#16467)
* fix: cap GetVideoBitrateParamValue at 400 Mbps

The previous cap of int.MaxValue / 2 (~1073 Mbps) is far beyond any
realistic transcode target and allows encoder parameters derived from
it (e.g. -bufsize = bitrate * 4 for QSV) to grow to multi-gigabit
values, which is incorrect regardless of whether the encoder tolerates it.

400 Mbps is a safe upper bound for all current hardware encoders:
- Intel QSV H.264 peaks at ~300 Mbps (High 5.1 CPB = 168.75 Mbit)
- HEVC High Tier Level 5.x supports ~240 Mbps
- AV1 hardware encoders have no meaningful real-world constraint at
  this level

The existing FallbackMaxStreamingBitrate mechanism (default 30 Mbps)
provides a similar guard but only when LiveStreamId is set, covering
M3U and HDHR sources. Plugin-provided streams and any source that
bypasses the LiveTV pipeline are not subject to it and can pass
unreasonably high values downstream. This cap closes that gap for
all encoder paths.

Suggested by @nyanmisaka in review of #16376.

* Update MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

---------

Co-authored-by: Bond-009 <bond.009@outlook.com>
2026-03-29 12:39:16 +02:00
furdiburd
921a364bb0 Add ignore patterns for Hungarian sample files (#16238)
* Add ignore patterns for Hungarian sample files

Added ignore patterns for Hungarian sample files.

* Removed leftover spaces
2026-03-29 12:38:46 +02:00
Tim Eisele
ad9ebe5baa More robust date handling in Library DB migration (#16474)
* More robust date handling in Library DB migration

* Apply review comment
2026-03-29 12:38:32 +02:00
Tim Eisele
6ea77f484d Fix attachment extraction of files without video or audio stream (#16312)
* Fix attachment extraction of files without video or audio stream

* Apply review suggestions
2026-03-29 12:38:01 +02:00
NoFear0411
e5bbb1ea0c Add spec-compliant dvh1 HLS variant for Dolby Vision Profile 5 (#16362)
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
Merge Conflict Labeler / Labeling (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
* Add spec-compliant dvh1 HLS variant for Dolby Vision Profile 5

DV Profile 5 has no backward-compatible base layer, so
SUPPLEMENTAL-CODECS cannot be used. The master playlist
currently labels P5 streams as hvc1 in the CODECS field,
even though DynamicHlsController already passes
-tag✌️0 dvh1 -strict -2 to FFmpeg for P5 copy-codec
streams, writing a dvh1 FourCC and dvvC configuration box
into the fMP4 init segment. This mismatch between the
manifest (hvc1) and the bitstream (dvh1) causes
spec-compliant clients like Apple TV and webOS 24+ to set
up an HDR10 pipeline instead of a Dolby Vision one.

Add a dvh1 variant before the existing hvc1 variant for P5
copy-codec streams. Both variants point to the same stream
URL. Spec-compliant clients select dvh1 and activate the
DV decoder path. Legacy clients that reject dvh1 in CODECS
fall through to the hvc1 variant and detect DV from the
init segment, preserving existing behavior.

Fixes #16179

* Address review: support AV1 DoVi P10, add client capability check

- GetDoviString: add isAv1 parameter, return dav1 FourCC for AV1 DoVi
  (P10 bl_compat_id=0) and dvh1 for HEVC DoVi (P5)
- Remove redundant IsDovi() check; VideoRangeType.DOVI is sufficient
  and correctly limits to profiles without a compatible base layer
- Replace IsDoviRemoved() with client capability check using
  GetRequestedRangeTypes(state.VideoStream.Codec) to only emit the
  dvh1/dav1 variant for clients that declared DOVI support
- Update comments and doc summary to reflect P5 + P10/bl0 scope

* Use codec string instead of boolean for DoVi FourCC mapping

Replace bool isAv1 with string codec in GetDoviString for
future-proofing when DoVi extends to H.266/VVC or AV2.

* Move AppendDoviPlaylist next to AppendPlaylist

* Fix SA1508: remove blank line before closing brace

* Use AppendLine() instead of Append(Environment.NewLine)
2026-03-28 22:12:06 +01:00
Patrick Cunniff
3fb1d94038 reverse check for track changed
Signed-off-by: Patrick Cunniff <pjcfifa@gmail.com>
2026-03-28 16:25:01 -04:00
Bond-009
e807575dc7 Merge pull request #16471 from jellyfin/renovate/swashbuckle-aspnetcore-monorepo
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (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 - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (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 swashbuckle-aspnetcore monorepo to 10.1.7
2026-03-27 19:53:59 +01:00
Bond-009
ef83000b30 Merge pull request #16483 from jellyfin/renovate/pin-dependencies
Pin chrisdickinson/setup-yq action to fa3192e
2026-03-27 19:53:12 +01:00
Bond-009
6a20f948f4 Merge pull request #16222 from tyage/fix/filename-truncation-bracketed-tags
Fix filename truncation when bracketed tags appear mid-filename
2026-03-27 19:21:43 +01:00
Bond-009
bd6acb1686 Merge pull request #16442 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.35.1
2026-03-27 19:16:25 +01:00
theguymadmax
f33c039d1b Fix BoxSet parentId being ignored in item queries 2026-03-27 13:48:30 -04:00
renovate[bot]
40cadfca44 Update github/codeql-action action to v4.35.1 2026-03-27 17:36:22 +00:00
Joshua M. Boniface
0ba41754d8 Merge pull request #15841 from JanzenJohn/feat/fix-ipv6-crash
fix crashes on devices that don't support ipv6
2026-03-27 13:33:12 -04:00
renovate[bot]
6e154de954 Pin chrisdickinson/setup-yq action to fa3192e 2026-03-26 19:27:09 +00:00
Lofuuzi
9c09e7113e Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Merge Conflict Labeler / Labeling (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
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/zh_Hant_HK/
2026-03-26 00:23:07 +00:00
renovate[bot]
5fa865f9e5 Update swashbuckle-aspnetcore monorepo to 10.1.7 2026-03-25 23:01:19 +00:00
Bond-009
bcc748e664 Update swashbuckle-aspnetcore monorepo to 10.1.6 (#16461)
Some checks failed
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
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 18:28:03 +01:00
dkanada
6b443bb2ec split openapi workflows between pull request and merge 2026-03-25 15:39:31 +09:00
renovate[bot]
8fc6f07d5a Update swashbuckle-aspnetcore monorepo to 10.1.6 2026-03-24 21:59:16 +00:00
scheilch
386c4cb723 Fix int32 overflow in QSV rate-control parameter computation (#16376)
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
Fix int32 overflow in QSV rate-control parameter computation
2026-03-24 18:02:00 +01:00
theguymadmax
24ec04d89f Backport pull request #16449 from jellyfin/release-10.11.z
Some checks failed
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
Project Automation / Project board (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Fix NFO saver using wrong provider ID for collectionnumber

Original-merge: ce867f9834

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-23 17:06:23 -04:00
nyanmisaka
b82a2ced75 Backport pull request #16423 from jellyfin/release-10.11.z
Fix readrate options in FFmpeg 8.1

Original-merge: 29b2361857

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-03-23 17:06:22 -04:00
Bond-009
568e26c534 Merge pull request #15593 from dkanada/book-provider-ids
include external IDs and URLs for book providers
2026-03-23 21:25:11 +01:00
lednurb
f52005768a Translated using Weblate (Dutch)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Merge Conflict Labeler / Labeling (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
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/nl/
2026-03-22 16:15:43 +00:00
Lofuuzi
f6211a03dd Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Some checks failed
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
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (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
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2026-03-20 19:32:38 +00:00
Louis
995d56d5ff Recognize ".m4b", ".m4a", ".aac", ".flac", ".mp3", and ".opus" as an audio-book formats (#15377)
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
* Recognize ".m4b" as an audio-book format

- Has the resolver recognize the ".m4b" format as book
  - Jellyfin reverts to seeing the file as a music file without this check

* Recognize ".m4a", ".aac", ".flac", and ".mp3" as an audio-book formats

- All the formats supported in the docs will now be marked as type "Book"

* Add ".opus" as a supported Audiobook format

---------

Co-authored-by: Louis Sandoval <louis@sandoval.family>
2026-03-19 20:31:29 +01:00
Lofuuzi
63a7c71e77 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
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/zh_Hant_HK/
2026-03-19 10:34:54 +00:00
Shadowghost
98bbc26c5e Add callback for segment data pruning to IMediaSegmentProvider 2026-03-14 19:58:43 +01:00
Shadowghost
382db1da0d Cleanup trickplay cache dir on failure 2026-03-07 10:49:42 +01:00
Bond_009
8824f07e1b Don't spam debug log with items without rating 2026-03-04 20:14:21 +01:00
tyage
1c2f08bc17 Fix filename truncation when bracketed tags appear mid-filename 2026-02-12 00:23:44 +09:00
dkanada
0413a8b6d2 include external IDs and URLs for book providers 2026-01-29 19:45:31 +09:00
ZeusCraft10
72b4faa00b Fix UDP Auto-Discovery returning IPv6 for cross-subnet IPv4 requests
Fixes #15898

When a UDP discovery request is relayed from a different IPv4 subnet,
GetBindAddress() now correctly returns an IPv4 address instead of
incorrectly falling back to ::1.

Changes:
- Loopback fallback now prefers address family matching the source IP
- Interface fallback now prefers interfaces matching source address family
- Added test case for cross-subnet IPv4 request scenario
2025-12-30 17:34:05 -05:00
john janzen
146681f0ba Warn server administrator when IPv6 is enabled but unsupported by OS 2025-12-21 15:37:22 +01:00
john janzen
93902fc610 fix crashes on devices that don't support ipv6 2025-12-20 19:50:33 +01:00
76 changed files with 1308 additions and 622 deletions

View File

@@ -87,13 +87,9 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- 10.11.8
- 10.11.7
- 10.11.6
- 10.11.5
- 10.11.4
- 10.11.3
- 10.11.2
- 10.11.1
- 10.11.0
- Master
- Unstable
- Older*

View File

@@ -28,13 +28,13 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1

44
.github/workflows/openapi-generate.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: OpenAPI Generate
on:
workflow_call:
inputs:
ref:
required: true
type: string
repository:
required: true
type: string
artifact:
required: true
type: string
permissions:
contents: read
jobs:
main:
name: Main
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref }}
repository: ${{ inputs.repository }}
- name: Configure .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
- name: Create File
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests
- name: Upload Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ inputs.artifact }}
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
retention-days: 14
if-no-files-found: error

View File

@@ -1,118 +1,28 @@
name: OpenAPI
name: OpenAPI Publish
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-base:
name: OpenAPI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-diff:
publish-openapi:
name: OpenAPI - Publish Artifact
uses: ./.github/workflows/openapi-generate.yml
permissions:
pull-requests: write
name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- openapi-head
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-base
path: openapi-base
- name: Detect OpenAPI changes
id: openapi-diff
uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0
with:
old-spec: openapi-base/openapi.json
new-spec: openapi-head/openapi.json
markdown: openapi-changelog.md
add-pr-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }}
contents: read
with:
ref: ${{ github.sha }}
repository: ${{ github.repository }}
artifact: openapi-head
publish-unstable:
name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
- publish-openapi
steps:
- name: Set unstable dated version
id: version
@@ -173,7 +83,7 @@ jobs:
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
- publish-openapi
steps:
- name: Set version number
id: version

View File

@@ -0,0 +1,80 @@
name: OpenAPI Check
on:
pull_request:
jobs:
ancestor:
name: Common Ancestor
runs-on: ubuntu-latest
outputs:
base_ref: ${{ steps.ancestor.outputs.base_ref }}
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Search History
id: ancestor
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} HEAD)
echo "ref: ${ANCESTOR_REF}"
echo "base_ref=${ANCESTOR_REF}" >> "$GITHUB_OUTPUT"
head:
name: Head Artifact
uses: ./.github/workflows/openapi-generate.yml
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
artifact: openapi-head
base:
name: Base Artifact
uses: ./.github/workflows/openapi-generate.yml
needs:
- ancestor
with:
ref: ${{ needs.ancestor.outputs.base_ref }}
repository: ${{ github.event.pull_request.base.repo.full_name }}
artifact: openapi-base
diff:
name: Generate Report
runs-on: ubuntu-latest
needs:
- head
- base
steps:
- name: Download Head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
- name: Download Base
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-base
path: openapi-base
- name: Detect Changes
id: openapi-diff
run: |
sed -i 's:allOf:oneOf:g' openapi-head/openapi.json
sed -i 's:allOf:oneOf:g' openapi-base/openapi.json
mkdir -p /tmp/openapi-report
mv openapi-head/openapi.json /tmp/openapi-report/head.json
mv openapi-base/openapi.json /tmp/openapi-report/base.json
docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md
- name: Upload Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: openapi-report
path: /tmp/openapi-report/openapi-report.md

View File

@@ -0,0 +1,59 @@
name: OpenAPI Report
on:
workflow_run:
workflows:
- OpenAPI Check
types:
- completed
jobs:
metadata:
name: Generate Metadata
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
pr_number: ${{ steps.pr_number.outputs.pr_number }}
steps:
- name: Get Pull Request Number
id: pr_number
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
API_RESPONSE=$(gh pr list --repo "${GITHUB_REPOSITORY}" --search "${HEAD_SHA}" --state open --json number)
PR_NUMBER=$(echo "${API_RESPONSE}" | jq '.[0].number')
echo "repository: ${GITHUB_REPOSITORY}"
echo "sha: ${HEAD_SHA}"
echo "response: ${API_RESPONSE}"
echo "pr: ${PR_NUMBER}"
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
comment:
name: Pull Request Comment
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs:
- metadata
permissions:
pull-requests: write
actions: read
contents: read
steps:
- name: Download OpenAPI Report
id: download_report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-report
path: openapi-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Push Comment
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
with:
github-token: ${{ secrets.JF_BOT_TOKEN }}
file-path: ${{ steps.download_report.outputs.download-path }}/openapi-report.md
pr-number: ${{ needs.metadata.outputs.pr_number }}
comment-tag: openapi-report

View File

@@ -28,7 +28,7 @@ jobs:
timeoutSeconds: 3600
- name: Setup YQ
uses: chrisdickinson/setup-yq@latest
uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest
with:
yq-version: v4.9.8

View File

@@ -164,6 +164,7 @@
- [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [ZachPhelan](https://github.com/ZachPhelan)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)

View File

@@ -47,7 +47,7 @@
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -75,11 +75,11 @@
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.5" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.11.0" />
<PackageVersion Include="z440.atl.core" Version="7.12.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />

View File

@@ -152,8 +152,8 @@ namespace Emby.Naming.Common
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>.+?)[ _\,\.\(\)\[\]\-](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)(?=[ _\,\.\(\)\[\]\-]|$)",
@"^\s*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",

View File

@@ -44,7 +44,7 @@ namespace Emby.Naming.Video
var match = expression.Match(name);
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
{
newName = cleaned.Value;
newName = cleaned.Value.Trim();
return true;
}

View File

@@ -217,6 +217,8 @@ namespace Emby.Naming.Video
// The CleanStringParser should have removed common keywords etc.
return testFilename.IsEmpty
|| testFilename[0] == '-'
|| testFilename[0] == '_'
|| testFilename[0] == '.'
|| CheckMultiVersionRegex().IsMatch(testFilename);
}
}

View File

@@ -1019,6 +1019,15 @@ namespace Emby.Server.Implementations.Dto
{
dto.AlbumId = albumParent.Id;
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
if (albumParent.LUFS.HasValue)
{
// -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
}
else if (albumParent.NormalizationGain.HasValue)
{
dto.AlbumNormalizationGain = albumParent.NormalizationGain;
}
}
// if (options.ContainsField(ItemFields.MediaSourceCount))

View File

@@ -31,6 +31,20 @@ namespace Emby.Server.Implementations.Library
"**/*.sample.?????",
"**/sample/*",
// Avoid adding Hungarian sample files
// https://github.com/jellyfin/jellyfin/issues/16237
"**/minta.?",
"**/minta.??",
"**/minta.???", // Matches minta.mkv
"**/minta.????", // Matches minta.webm
"**/minta.?????",
"**/*.minta.?",
"**/*.minta.??",
"**/*.minta.???",
"**/*.minta.????",
"**/*.minta.?????",
"**/minta/*",
// Directories
"**/metadata/**",
"**/metadata",

View File

@@ -70,12 +70,6 @@ namespace Emby.Server.Implementations.Library
return match ? imdbId.ToString() : null;
}
// Allow tmdb as an alias for tmdbid
if (attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase))
{
return str.GetAttributeValue("tmdb");
}
return null;
}

View File

@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
public class BookResolver : ItemResolver<Book>
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf", ".m4b", ".m4a", ".aac", ".flac", ".mp3", ".opus" };
protected override Book Resolve(ItemResolveArgs args)
{

View File

@@ -63,8 +63,8 @@
"Photos": "Fotos",
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "S'ha instal·lat {0}",
"PluginInstalledWithName": "{0} s'ha instal·lat",
"PluginUninstalledWithName": "{0} s'ha desinstal·lat",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",

View File

@@ -19,7 +19,7 @@
"HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteEpisodes": "Lieblingsfolgen",
"HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingssongs",
"HeaderLiveTV": "Live TV",

View File

@@ -133,8 +133,8 @@
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
"TaskExtractMediaSegments": "Skaneeri meediasegmente",
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
"TaskExtractMediaSegments": "Skaneeri meedialõike",
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Puhasta kasutajaandmed",
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."

View File

@@ -14,5 +14,9 @@
"DeviceOnlineWithName": "{0} er sambundið",
"Favorites": "Yndis",
"Folders": "Mappur",
"Forced": "Kravt"
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
"HeaderFavoriteSongs": "Yndissangir",
"LabelIpAddressValue": "IP atsetur: {0}"
}

View File

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

View File

@@ -9,46 +9,46 @@
"Artists": "არტისტი",
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
"Books": "წიგნები",
"Forced": "ძალით",
"Forced": "იძულებით",
"Inherit": "მემკვიდრეობით",
"Latest": "უახლესი",
"Movies": "ფილმები",
"Music": "მუსიკა",
"Photos": "ფოტოები",
"Playlists": "დასაკრავი სიები",
"Plugin": "დამატება",
"Plugin": "მოდული",
"Shows": "სერიალები",
"Songs": "სიმღერები",
"Sync": "სინქრონიზაცია",
"System": "სისტემა",
"Undefined": "აღუწერელი",
"Undefined": "განუსაზღვრელი",
"User": "მომხმარებელი",
"TasksMaintenanceCategory": "რემონტი",
"TasksLibraryCategory": "ბიბლიოთეკა",
"ChapterNameValue": "თავი {0}",
"HeaderContinueWatching": "ყურების გაგრძელება",
"HeaderFavoriteArtists": "რჩეული შემსრულებლები",
"DeviceOfflineWithName": "{0} გათიშა",
"DeviceOfflineWithName": "{0} გამოეთიშა",
"External": "გარე",
"HeaderFavoriteEpisodes": "რჩეული ეპიზოდები",
"HeaderFavoriteSongs": "რჩეული სიმღერები",
"HeaderRecordingGroups": "ჩამწერი ჯგუფები",
"HearingImpaired": "სმენადაქვეითებული",
"LabelRunningTimeValue": "გაშვებულობის დრო: {0}",
"LabelRunningTimeValue": "ხანგრძლივობა: {0}",
"MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე",
"MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა",
"MixedContent": "შერეული შემცველობა",
"MusicVideos": "მუსიკი ვიდეოები",
"MusicVideos": "მუსიკალური ვიდეოები",
"NotificationOptionInstallationFailed": "დაყენების შეცდომა",
"NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია",
"NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია",
"NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია",
"NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია",
"PluginUninstalledWithName": "{0} წაიშალა",
"ScheduledTaskStartedWithName": "{0} გაეშვა",
"ScheduledTaskStartedWithName": "{0} დაიწყო",
"VersionNumber": "ვერსია {0}",
"TasksChannelsCategory": "ინტერნეტ-არხები",
"ValueSpecialEpisodeName": "სპეციალური - {0}",
"ValueSpecialEpisodeName": "დამატებითი - {0}",
"TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.",
"Channels": "არხები",
"Collections": "კოლექციები",
@@ -56,31 +56,31 @@
"Favorites": "რჩეულები",
"Folders": "საქაღალდეები",
"HeaderFavoriteShows": "რჩეული სერიალები",
"HeaderLiveTV": "ცოცხალი TV",
"HeaderNextUp": "შემდეგი ზემოთ",
"HeaderLiveTV": "ლაივ ტელევიზია",
"HeaderNextUp": "შემდეგი",
"HomeVideos": "სახლის ვიდეოები",
"NameSeasonNumber": "სეზონი {0}",
"NameSeasonUnknown": "სეზონი უცნობია",
"NotificationOptionPluginError": "დამატების შეცდომა",
"NotificationOptionPluginInstalled": "დამატება დაყენებულია",
"NotificationOptionPluginUninstalled": "დამატება წაიშალა",
"NotificationOptionPluginError": "მოდულის შეცდომა",
"NotificationOptionPluginInstalled": "მოდული დაყენებულია",
"NotificationOptionPluginUninstalled": "მოდული წაიშალა",
"ProviderValue": "მომწოდებელი: {0}",
"ScheduledTaskFailedWithName": "{0} ავარიულა",
"TvShows": "TV სერიალები",
"ScheduledTaskFailedWithName": "{0} ვერ შესრულა",
"TvShows": "სატელევიზიო სერიალები",
"TaskRefreshPeople": "ხალხის განახლება",
"TaskUpdatePlugins": "დამატებების განახლება",
"TaskUpdatePlugins": "მოდულების განახლება",
"TaskRefreshChannels": "არხების განახლება",
"TaskOptimizeDatabase": "ბაზების ოპტიმიზაცია",
"TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია",
"TaskKeyframeExtractor": "საკვანძო კადრის გამომღები",
"DeviceOnlineWithName": "{0} შეერთებულია",
"DeviceOnlineWithName": "{0} დაკავშირდა",
"LabelIpAddressValue": "IP მისამართი: {0}",
"NameInstallFailed": "{0}-ის დაყენების შეცდომა",
"NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება",
"NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია",
"NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია",
"NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია",
"NotificationOptionServerRestartRequired": "სერვერის გადატვირთვა აუცილებელია",
"NotificationOptionTaskFailed": "დაგეგმილი ამოცანის შეცდომა",
"NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია",
"NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა",
"NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა",
"NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა",
"NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია",
"PluginInstalledWithName": "{0} დაყენებულია",
@@ -91,39 +91,51 @@
"TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება",
"TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება",
"TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება",
"TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა",
"UserDownloadingItemWithValues": "{0} -ი {0}-ს იწერს",
"FailedLoginAttemptWithUserName": "{0}-დან შემოსვლის შეცდომა",
"TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა",
"UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს",
"FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან",
"MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა",
"MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა",
"ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა",
"UserCreatedWithName": "მომხმარებელი {0} შეიქმნა",
"UserDeletedWithName": "მომხმარებელი {0} წაშლილია",
"UserOnlineFromDevice": "{0}-ი ხაზზეა {1}-დან",
"UserOfflineFromDevice": "{0}-ი {1}-დან გათიშა",
"UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან",
"UserOfflineFromDevice": "{0}-ი {1}-დან გათიშა",
"ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში",
"ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან",
"UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია",
"UserStartedPlayingItemWithValues": "{0} თამაშობს {1}-ს {2}-ზე",
"UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია",
"UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე",
"UserPasswordChangedWithName": "მომხმარებლი {0}-სთვის პაროლი შეცვლა",
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
"UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე",
"UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე",
"TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
"TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
"NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
"CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
"StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა",
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერა ვერ შესრულდა",
"ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
"TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერების წაშლა.",
"TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.",
"TaskRefreshLibraryDescription": "თქვენ მედი ბიბლიოთეკაში ახალი ფაილებ ძებნა და მეტამონაცემების განახლება.",
"TaskCleanActivityLogDescription": "შლის მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერებ.",
"TaskCleanCacheDescription": "შლის სისტემისთვის არასაჭირო ქეშის ფაილებ.",
"TaskRefreshLibraryDescription": "ეძებს ახალ ფაილებს თქვენ მედიის ბიბლიოთეკაში და ანახლებს მეტამონაცემებ.",
"TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
"TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული მოდულების განახლებების გადმოწერა და დაყენება.",
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრებძებნა.",
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლებ. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
"TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.",
"TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.",
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება",
"TaskAudioNormalization": "აუდიოს ნორმალიზება",
"TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.",
"TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა",
"TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის",
"TaskCleanCollectionsAndPlaylists": "კოლექციების და დასაკრავი სიების გასუფთავება",
"TaskCleanCollectionsAndPlaylistsDescription": "შლის არარსებულ ერთეულებს კოლექციებიდან და დასაკრავი სიებიდან.",
"TaskExtractMediaSegments": "მედია სეგმენტების სკანირება",
"TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.",
"TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია",
"TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.",
"CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება",
"CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ."
}

View File

@@ -1,5 +1,4 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Applicatie",
"Artists": "Artiesten",
@@ -14,19 +13,18 @@
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten",
"HeaderContinueWatching": "Verderkijken",
"HeaderContinueWatching": "Verder kijken",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
"HeaderFavoriteShows": "Favoriete series",
"HeaderFavoriteSongs": "Favoriete nummers",
"HeaderLiveTV": "Live-tv",
"HeaderNextUp": "Als volgende",
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
"HomeVideos": "Homevideo's",
"Inherit": "Erven",
"Inherit": "Overnemen",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
"LabelIpAddressValue": "IP-adres: {0}",
@@ -116,7 +114,7 @@
"TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Gedwongen",
"Forced": "Geforceerd",
"Default": "Standaard",
"TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
"TaskOptimizeDatabase": "Database optimaliseren",
@@ -137,5 +135,7 @@
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
"CleanupUserDataTask": "Opruimtaak gebruikersdata"
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
"Albums": "Albums",
"Genres": "Genres"
}

View File

@@ -124,17 +124,17 @@
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
"External": "Зовнішній",
"HearingImpaired": "З порушеннями слуху",
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
"TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
"TaskRefreshTrickplayImagesDescription": "Створює прев'ю-зображення для відео у ввімкнених медіатеках.",
"TaskRefreshTrickplayImages": "Створити Прев'ю-зображення",
"TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
"TaskAudioNormalization": "Нормалізація аудіо",
"TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень",
"TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень",
"TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.",
"TaskMoveTrickplayImagesDescription": "Переміщує наявні прев'ю-зображення відповідно до налаштувань медіатеки.",
"TaskExtractMediaSegments": "Сканування медіа-сегментів",
"TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень",
"TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень",
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
"CleanupUserDataTask": "Завдання очищення даних користувача",
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому."

View File

@@ -3,38 +3,38 @@
"AppDeviceValues": "程式:{0},裝置:{1}",
"Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "成功授權 {0}",
"AuthenticationSucceededWithUserName": "{0} 成功通過驗證",
"Books": "書籍",
"CameraImageUploadedFrom": "{0} 已經成功上咗一張新相",
"CameraImageUploadedFrom": "{0} 已經成功上咗一張新相",
"Channels": "頻道",
"ChapterNameValue": "第 {0} 章",
"Collections": "系列",
"DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
"DeviceOfflineWithName": "{0} 斷開咗連線",
"DeviceOnlineWithName": "{0} 連線咗",
"FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗",
"Favorites": "我的最愛",
"Favorites": "心水",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯歌手",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛的專輯",
"HeaderFavoriteArtists": "最愛的藝人",
"HeaderFavoriteEpisodes": "最愛的劇集",
"HeaderFavoriteShows": "最愛的節目",
"HeaderFavoriteSongs": "最愛的歌曲",
"HeaderContinueWatching": "繼續睇返",
"HeaderFavoriteAlbums": "心水嘅專輯",
"HeaderFavoriteArtists": "心水嘅藝人",
"HeaderFavoriteEpisodes": "心水嘅劇集",
"HeaderFavoriteShows": "心水嘅節目",
"HeaderFavoriteSongs": "心水嘅歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "繼續觀看",
"HeaderNextUp": "跟住落嚟",
"HeaderRecordingGroups": "錄製組",
"HomeVideos": "家庭影片",
"Inherit": "繼承",
"ItemAddedWithName": "{0} 經已加咗入媒體",
"ItemRemovedWithName": "{0} 經已由媒體移除咗",
"ItemAddedWithName": "{0} 經已加咗入媒體",
"ItemRemovedWithName": "{0} 經已由媒體移除咗",
"LabelIpAddressValue": "IP 地址:{0}",
"LabelRunningTimeValue": "運時間:{0}",
"LabelRunningTimeValue": "運時間:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin 經已更新咗",
"MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」經更新咗",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」經更新咗",
"MessageServerConfigurationUpdated": "伺服器設定經已更新咗",
"MixedContent": "混合內容",
"Movies": "電影",
@@ -42,100 +42,100 @@
"MusicVideos": "MV",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季度",
"NameSeasonUnknown": "未知季度",
"NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的更新",
"NotificationOptionApplicationUpdateInstalled": "完成更新應用程式",
"NotificationOptionAudioPlayback": "播放音訊",
"NotificationOptionAudioPlaybackStopped": "停播放音訊",
"NotificationOptionCameraImageUploaded": "相片上傳",
"NotificationOptionApplicationUpdateAvailable": "有得更新應用程式",
"NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗",
"NotificationOptionAudioPlayback": "開始播放音訊",
"NotificationOptionAudioPlaybackStopped": "停播放音訊",
"NotificationOptionCameraImageUploaded": "相機相片上載咗",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "新增媒體",
"NotificationOptionPluginError": "插件錯誤",
"NotificationOptionPluginInstalled": "安裝插件",
"NotificationOptionPluginUninstalled": "解除安裝插件",
"NotificationOptionPluginUpdateInstalled": "完成更新插件",
"NotificationOptionServerRestartRequired": "伺服器需要重",
"NotificationOptionTaskFailed": "排程工作執行失敗",
"NotificationOptionUserLockedOut": "封鎖用戶",
"NotificationOptionVideoPlayback": "播放影片",
"NotificationOptionVideoPlaybackStopped": "停播放影片",
"NotificationOptionNewLibraryContent": "加咗新內容",
"NotificationOptionPluginError": "外掛程式錯誤",
"NotificationOptionPluginInstalled": "安裝外掛程式",
"NotificationOptionPluginUninstalled": "解除安裝外掛程式",
"NotificationOptionPluginUpdateInstalled": "外掛程式更新好咗",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程工作失敗",
"NotificationOptionUserLockedOut": "用家被鎖定咗",
"NotificationOptionVideoPlayback": "開始播放影片",
"NotificationOptionVideoPlaybackStopped": "停播放影片",
"Photos": "相片",
"Playlists": "播放清單",
"Plugin": "插件",
"PluginInstalledWithName": "已安裝 {0}",
"PluginUninstalledWithName": "已移除 {0}",
"PluginUpdatedWithName": "更新 {0}",
"Plugin": "外掛程式",
"PluginInstalledWithName": "裝好咗 {0}",
"PluginUninstalledWithName": "剷走咗 {0}",
"PluginUpdatedWithName": "更新好咗 {0}",
"ProviderValue": "提供者:{0}",
"ScheduledTaskFailedWithName": "{0} 執行失敗",
"ScheduledTaskStartedWithName": "開始執行 {0}",
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,稍後再試。",
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,唔該稍後再試。",
"SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗",
"Sync": "同步",
"System": "系統",
"TvShows": "電視節目",
"User": "用戶",
"UserCreatedWithName": "經已建立咗新用戶 {0}",
"UserDeletedWithName": "用戶 {0} 經被刪",
"User": "使用者",
"UserCreatedWithName": "經已建立咗新使用者 {0}",
"UserDeletedWithName": "使用者 {0} 經被刪",
"UserDownloadingItemWithValues": "{0} 下載緊 {1}",
"UserLockedOutWithName": "用戶 {0} 經已被鎖定",
"UserLockedOutWithName": "使用者 {0} 經已被鎖定",
"UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線",
"UserOnlineFromDevice": "{0} 正喺 {1} 連線",
"UserPasswordChangedWithName": "用戶 {0} 嘅密碼經更改咗",
"UserPolicyUpdatedWithName": "用戶 {0} 嘅權限經更新咗",
"UserPasswordChangedWithName": "使用者 {0} 嘅密碼經更改咗",
"UserPolicyUpdatedWithName": "使用者 {0} 嘅權限經更新咗",
"UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體",
"ValueSpecialEpisodeName": "特 - {0}",
"ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體",
"ValueSpecialEpisodeName": "特 - {0}",
"VersionNumber": "版本 {0}",
"TaskDownloadMissingSubtitles": "下載欠缺字幕",
"TaskUpdatePlugins": "更新插件",
"TaskDownloadMissingSubtitles": "下載漏咗嘅字幕",
"TaskUpdatePlugins": "更新外掛程式",
"TasksApplicationCategory": "應用程式",
"TaskRefreshLibraryDescription": "掃描媒體嚟搵新檔案,同時重新載入元數據。",
"TaskRefreshLibraryDescription": "掃描媒體嚟搵新檔案,同時重新載入媒體詳細資料。",
"TasksMaintenanceCategory": "維護",
"TaskDownloadMissingSubtitlesDescription": "根據元數據設定,喺網上幫你搵返啲欠缺嘅字幕。",
"TaskDownloadMissingSubtitlesDescription": "根據媒體詳細資料設定,喺網上幫你搵返啲欠缺嘅字幕。",
"TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。",
"TaskRefreshChannels": "重新載入頻道",
"TaskCleanTranscodeDescription": "自動刪超過一日嘅轉碼檔案。",
"TaskCleanTranscode": "清理轉碼資料夾",
"TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅插件進行下載同安裝。",
"TaskRefreshPeopleDescription": "更新媒體入面演員同導演嘅元數據。",
"TaskCleanLogsDescription": "自動刪超過 {0} 日嘅紀錄檔。",
"TaskCleanLogs": "清理紀錄檔資料夾",
"TaskRefreshLibrary": "掃描媒體",
"TaskCleanTranscodeDescription": "自動刪超過一日嘅轉碼檔案。",
"TaskCleanTranscode": "清理轉碼資料夾",
"TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅外掛程式進行下載同安裝。",
"TaskRefreshPeopleDescription": "更新媒體入面演員同導演嘅媒體詳細資料。",
"TaskCleanLogsDescription": "自動刪超過 {0} 日嘅紀錄檔。",
"TaskCleanLogs": "清理日誌資料夾",
"TaskRefreshLibrary": "掃描媒體",
"TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。",
"TaskRefreshChapterImages": "取章節圖",
"TaskCleanCacheDescription": "刪系統已經唔再需要嘅緩存檔案。",
"TaskCleanCache": "清理緩存資料夾",
"TasksChannelsCategory": "網頻道",
"TasksLibraryCategory": "媒體",
"TaskRefreshChapterImages": "取章節圖",
"TaskCleanCacheDescription": "刪系統已經唔再需要嘅快取檔案。",
"TaskCleanCache": "清理快取Cache資料夾",
"TasksChannelsCategory": "網頻道",
"TasksLibraryCategory": "媒體",
"TaskRefreshPeople": "重新載入人物",
"TaskCleanActivityLog": "清理活動錄",
"TaskCleanActivityLog": "清理活動錄",
"Undefined": "未定義",
"Forced": "強制",
"Default": "預設",
"TaskOptimizeDatabaseDescription": "壓縮數據並釋放剩餘空間。喺掃描媒體或者做咗一啲會修改數據嘅操作之後行呢個任務,或者可以提升效能。",
"TaskOptimizeDatabase": "最佳化數據",
"TaskCleanActivityLogDescription": "刪超過設定日期嘅活動記錄。",
"Default": "初始",
"TaskOptimizeDatabaseDescription": "壓縮數據並釋放剩餘空間。喺掃描媒體或者做咗一啲會修改數據嘅操作之後行呢個任務,或者可以提升效能。",
"TaskOptimizeDatabase": "最佳化數據",
"TaskCleanActivityLogDescription": "刪超過設定日期嘅活動記錄。",
"TaskKeyframeExtractorDescription": "提取關鍵影格Keyframe嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。",
"TaskKeyframeExtractor": "關鍵影格提取器",
"External": "外部",
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
"TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體影片製作快轉預覽圖。",
"TaskRefreshTrickplayImages": "產生搜畫預覽圖",
"TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體影片製作快轉預覽圖。",
"TaskExtractMediaSegments": "掃描媒體分段資訊",
"TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。",
"TaskDownloadMissingLyrics": "下載缺歌詞",
"TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅外掛程式入面提取媒體片段。",
"TaskDownloadMissingLyrics": "下載缺失嘅歌詞",
"TaskDownloadMissingLyricsDescription": "幫啲歌下載歌詞",
"TaskCleanCollectionsAndPlaylists": "清理媒體系列Collections同埋播放清單",
"TaskAudioNormalization": "音訊同等化",
"TaskAudioNormalizationDescription": "掃描檔案入面嘅音訊標准化Audio Normalization數據。",
"TaskCleanCollectionsAndPlaylistsDescription": "自動清理資料庫同播放清單入面已經唔存在嘅項目。",
"TaskMoveTrickplayImagesDescription": "根據媒體設定,將現有嘅 Trickplay快轉預覽檔案搬去對應位置。",
"TaskMoveTrickplayImagesDescription": "根據媒體設定,將現有嘅 Trickplay快轉預覽檔案搬去對應位置。",
"TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置",
"CleanupUserDataTask": "清理用戶資料嘅任務",
"CleanupUserDataTaskDescription": "從用戶數據入面清除嗰啲已經被刪除咗超過 90 日嘅媒體相關資料。"
"CleanupUserDataTask": "清理使用者資料嘅任務",
"CleanupUserDataTaskDescription": "清理已消失至少 90 日嘅媒體用家數據(包括觀看狀態、心水狀態等)。"
}

View File

@@ -960,7 +960,7 @@ namespace Emby.Server.Implementations.Session
}
var tracksChanged = UpdatePlaybackSettings(user, info, data);
if (!tracksChanged)
if (tracksChanged)
{
changed = true;
}

View File

@@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStreamByContainer(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

View File

@@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -187,7 +187,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -206,8 +206,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -412,12 +412,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -427,7 +427,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -448,8 +448,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -585,12 +585,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -601,7 +601,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -620,8 +620,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -752,12 +752,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -767,7 +767,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -788,8 +788,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -921,12 +921,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -937,7 +937,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -956,8 +956,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1091,7 +1091,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1099,12 +1099,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1114,7 +1114,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1135,8 +1135,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1273,7 +1273,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1281,12 +1281,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1297,7 +1297,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1316,8 +1316,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

View File

@@ -11,6 +11,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
@@ -270,15 +271,17 @@ public class ItemsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (includeItemTypes.Length == 1
&& includeItemTypes[0] == BaseItemKind.BoxSet)
{
parentId = null;
}
var item = _libraryManager.GetParentItem(parentId, userId);
QueryResult<BaseItem> result;
if (includeItemTypes.Length == 1
&& includeItemTypes[0] == BaseItemKind.BoxSet
&& item is not BoxSet)
{
parentId = null;
item = _libraryManager.GetUserRootFolder();
}
if (item is not Folder folder)
{
folder = _libraryManager.GetUserRootFolder();

View File

@@ -454,7 +454,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.LiveTvManagement)]
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
{
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
@@ -976,7 +976,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created tuner host returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
[HttpPost("TunerHosts")]
[Authorize(Policy = Policies.LiveTvManagement)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
=> await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
@@ -988,7 +988,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Tuner host deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("TunerHosts")]
[Authorize(Policy = Policies.LiveTvManagement)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteTunerHost([FromQuery] string? id)
{
@@ -1021,7 +1021,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created listings provider returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
[HttpPost("ListingProviders")]
[Authorize(Policy = Policies.LiveTvManagement)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
@@ -1047,7 +1047,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Listing provider deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("ListingProviders")]
[Authorize(Policy = Policies.LiveTvManagement)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteListingProvider([FromQuery] string? id)
{
@@ -1080,7 +1080,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Available countries returned.</response>
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
[Authorize(Policy = Policies.LiveTvAccess)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Application.Json)]
public async Task<ActionResult> GetSchedulesDirectCountries()
@@ -1101,7 +1101,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Channel mapping options returned.</response>
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
[HttpGet("ChannelMappingOptions")]
[Authorize(Policy = Policies.LiveTvAccess)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
=> _listingsManager.GetChannelMappingOptions(providerId);
@@ -1113,7 +1113,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.LiveTvManagement)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
@@ -1137,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
[HttpGet("Tuners/Discover")]
[Authorize(Policy = Policies.LiveTvManagement)]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
=> _tunerHostManager.DiscoverTuners(newDevicesOnly);
@@ -1185,7 +1185,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesVideoFile]
public ActionResult GetLiveStreamFile(
[FromRoute, Required] string streamId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
[FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container)
{
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
if (liveStreamInfo is null)

View File

@@ -47,6 +47,7 @@ public class PersonsController : BaseJellyfinApiController
/// <summary>
/// Gets all persons.
/// </summary>
/// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
@@ -57,6 +58,7 @@ public class PersonsController : BaseJellyfinApiController
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
/// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific library. Omit to use the root.</param>
/// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
/// <param name="userId">User id.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
@@ -65,6 +67,7 @@ public class PersonsController : BaseJellyfinApiController
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
@@ -75,6 +78,7 @@ public class PersonsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery] Guid? parentId,
[FromQuery] Guid? appearsInItemId,
[FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true)
@@ -96,6 +100,8 @@ public class PersonsController : BaseJellyfinApiController
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
AppearsInItemId = appearsInItemId ?? Guid.Empty,
ParentId = parentId,
StartIndex = startIndex,
Limit = limit ?? 0
});

View File

@@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController
[FromBody, Required] NewGroupRequestDto requestData)
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim());
return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None));
}

View File

@@ -101,13 +101,13 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
[FromQuery] MediaStreamProtocol? transcodingProtocol,
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,

View File

@@ -313,18 +313,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -334,7 +334,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -355,8 +355,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -551,18 +551,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public Task<ActionResult> GetVideoStreamByContainer(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -572,7 +572,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -593,8 +593,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

View File

@@ -209,6 +209,25 @@ public class DynamicHlsHelper
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
}
// For DoVi profiles without a compatible base layer (P5 HEVC, P10/bl0 AV1),
// add a spec-compliant dvh1/dav1 variant before the hvc1 hack variant.
// SUPPLEMENTAL-CODECS cannot be used for these profiles (no compatible BL to supplement).
// The DoVi variant is listed first so spec-compliant clients (Apple TV, webOS 24+)
// select it over the fallback when both have identical BANDWIDTH.
// Only emit for clients that explicitly declared DOVI support to avoid breaking
// non-compliant players that don't recognize dvh1/dav1 CODECS strings.
if (state.VideoStream is not null
&& state.VideoRequest is not null
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRangeType == VideoRangeType.DOVI
&& state.VideoStream.DvProfile.HasValue
&& state.VideoStream.DvLevel.HasValue
&& state.GetRequestedRangeTypes(state.VideoStream.Codec)
.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase))
{
AppendDoviPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
}
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (state.VideoStream is not null && state.VideoRequest is not null)
@@ -355,6 +374,65 @@ public class DynamicHlsHelper
return playlistBuilder;
}
/// <summary>
/// Appends a Dolby Vision variant with dvh1/dav1 CODECS for profiles without a compatible
/// base layer (P5 HEVC, P10/bl0 AV1). This enables spec-compliant HLS clients to detect
/// DoVi from the manifest rather than relying on init segment inspection.
/// </summary>
/// <param name="builder">StringBuilder for the master playlist.</param>
/// <param name="state">StreamState of the current stream.</param>
/// <param name="url">Playlist URL for this variant.</param>
/// <param name="bitrate">Bitrate for the BANDWIDTH field.</param>
/// <param name="subtitleGroup">Subtitle group identifier, or null.</param>
private void AppendDoviPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
{
var dvProfile = state.VideoStream.DvProfile;
var dvLevel = state.VideoStream.DvLevel;
if (dvProfile is null || dvLevel is null)
{
return;
}
var playlistBuilder = new StringBuilder();
playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
.Append(",AVERAGE-BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
playlistBuilder.Append(",VIDEO-RANGE=PQ");
var dvCodec = HlsCodecStringHelpers.GetDoviString(dvProfile.Value, dvLevel.Value, state.ActualOutputVideoCodec);
string audioCodecs = string.Empty;
if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
{
audioCodecs = GetPlaylistAudioCodecs(state);
}
playlistBuilder.Append(",CODECS=\"")
.Append(dvCodec);
if (!string.IsNullOrEmpty(audioCodecs))
{
playlistBuilder.Append(',').Append(audioCodecs);
}
playlistBuilder.Append('"');
AppendPlaylistResolutionField(playlistBuilder, state);
AppendPlaylistFramerateField(playlistBuilder, state);
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
playlistBuilder.Append(",SUBTITLES=\"")
.Append(subtitleGroup)
.Append('"');
}
playlistBuilder.AppendLine();
playlistBuilder.AppendLine(url);
builder.Append(playlistBuilder);
}
/// <summary>
/// Appends a VIDEO-RANGE field containing the range of the output video stream.
/// </summary>

View File

@@ -346,4 +346,25 @@ public static class HlsCodecStringHelpers
return result.ToString();
}
/// <summary>
/// Gets a Dolby Vision codec string for profiles without a compatible base layer.
/// </summary>
/// <param name="dvProfile">Dolby Vision profile number.</param>
/// <param name="dvLevel">Dolby Vision level number.</param>
/// <param name="codec">Video codec name (e.g. "hevc", "av1") to determine the DoVi FourCC.</param>
/// <returns>Dolby Vision codec string.</returns>
public static string GetDoviString(int dvProfile, int dvLevel, string codec)
{
// HEVC DoVi uses dvh1, AV1 DoVi uses dav1 (out-of-band parameter sets, recommended by Apple HLS spec Rule 1.10)
var fourCc = string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1";
StringBuilder result = new StringBuilder(fourCc, 12);
result.Append('.')
.AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvProfile)
.Append('.')
.AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvLevel);
return result.ToString();
}
}

View File

@@ -17,9 +17,7 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Helpers;
@@ -422,14 +420,18 @@ public static class StreamingHelpers
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
break;
case 4:
if (videoRequest is not null)
if (videoRequest is not null && IsValidCodecName(val))
{
videoRequest.VideoCodec = val;
}
break;
case 5:
request.AudioCodec = val;
if (IsValidCodecName(val))
{
request.AudioCodec = val;
}
break;
case 6:
if (videoRequest is not null)
@@ -483,7 +485,7 @@ public static class StreamingHelpers
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
break;
case 15:
if (videoRequest is not null)
if (videoRequest is not null && EncodingHelper.LevelValidationRegex().IsMatch(val))
{
videoRequest.Level = val;
}
@@ -504,7 +506,7 @@ public static class StreamingHelpers
break;
case 18:
if (videoRequest is not null)
if (videoRequest is not null && IsValidCodecName(val))
{
videoRequest.Profile = val;
}
@@ -563,7 +565,11 @@ public static class StreamingHelpers
break;
case 30:
request.SubtitleCodec = val;
if (IsValidCodecName(val))
{
request.SubtitleCodec = val;
}
break;
case 31:
if (videoRequest is not null)
@@ -586,6 +592,11 @@ public static class StreamingHelpers
}
}
private static bool IsValidCodecName(string val)
{
return EncodingHelper.ContainerValidationRegex().IsMatch(val);
}
/// <summary>
/// Parses the container into its file extension.
/// </summary>

View File

@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.SyncPlayDtos;
/// <summary>
@@ -17,5 +19,6 @@ public class NewGroupRequestDto
/// Gets or sets the group name.
/// </summary>
/// <value>The name of the new group.</value>
[StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")]
public string GroupName { get; set; }
}

View File

@@ -185,7 +185,7 @@ public static class UserEntityExtensions
entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false));
entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));

View File

@@ -118,15 +118,21 @@ public class BackupService : IBackupService
throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
}
void CopyDirectory(string source, string target)
void CopyDirectory(string source, string target, string[]? exclude = null)
{
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray();
foreach (var item in zipArchive.Entries)
{
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal)))
{
continue;
}
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|| Path.EndsInDirectorySeparator(item.FullName))
@@ -142,8 +148,10 @@ public class BackupService : IBackupService
}
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
CopyDirectory("Data", _applicationPaths.DataPath);
CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]);
CopyDirectory("Root", _applicationPaths.RootFolderPath);
CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath);
CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath);
if (manifest.Options.Database)
{
@@ -404,6 +412,15 @@ public class BackupService : IBackupService
if (backupOptions.Metadata)
{
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
// If a custom metadata path is configured, the default location may still contain data.
if (!string.Equals(
Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath),
Path.GetFullPath(_applicationPaths.InternalMetadataPath),
StringComparison.OrdinalIgnoreCase))
{
CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default"));
}
}
var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false);

View File

@@ -62,7 +62,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
if (filter.StartIndex.HasValue && filter.StartIndex > 0)
{
dbQuery = dbQuery.Skip(filter.StartIndex.Value);
}
if (filter.Limit > 0)
{
dbQuery = dbQuery.Take(filter.Limit);
@@ -197,6 +201,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
}
if (filter.ParentId != null)
{
query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentId && i.ItemId == w.ItemId)));
}
if (!filter.AppearsInItemId.IsEmpty())
{
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));

View File

@@ -182,6 +182,18 @@ public class MediaSegmentManager : IMediaSegmentManager
/// <inheritdoc />
public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
{
foreach (var provider in _segmentProviders)
{
try
{
await provider.CleanupExtractedData(itemId, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Provider {ProviderName} failed to clean up extracted data for item {ItemId}", provider.Name, itemId);
}
}
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{

View File

@@ -399,64 +399,72 @@ public class TrickplayManager : ITrickplayManager
var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
var trickplayInfo = new TrickplayInfo
try
{
Width = width,
Interval = options.Interval,
TileWidth = options.TileWidth,
TileHeight = options.TileHeight,
ThumbnailCount = images.Count,
// Set during image generation
Height = 0,
Bandwidth = 0
};
/*
* Generate trickplay tiles from sets of thumbnails
*/
var imageOptions = new ImageCollageOptions
{
Width = trickplayInfo.TileWidth,
Height = trickplayInfo.TileHeight
};
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
for (int i = 0; i < requiredTiles; i++)
{
// Set output/input paths
var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tilePath;
imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
// Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
if (trickplayInfo.Height == 0)
var trickplayInfo = new TrickplayInfo
{
trickplayInfo.Height = height;
Width = width,
Interval = options.Interval,
TileWidth = options.TileWidth,
TileHeight = options.TileHeight,
ThumbnailCount = images.Count,
// Set during image generation
Height = 0,
Bandwidth = 0
};
/*
* Generate trickplay tiles from sets of thumbnails
*/
var imageOptions = new ImageCollageOptions
{
Width = trickplayInfo.TileWidth,
Height = trickplayInfo.TileHeight
};
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
for (int i = 0; i < requiredTiles; i++)
{
// Set output/input paths
var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tilePath;
imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
// Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
if (trickplayInfo.Height == 0)
{
trickplayInfo.Height = height;
}
// Update bitrate
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
}
// Update bitrate
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
/*
* Move trickplay tiles to output directory
*/
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
// Replace existing tiles if they already exist
if (Directory.Exists(outputDir))
{
Directory.Delete(outputDir, true);
}
_fileSystem.MoveDirectory(workDir, outputDir);
return trickplayInfo;
}
/*
* Move trickplay tiles to output directory
*/
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
// Replace existing tiles if they already exist
if (Directory.Exists(outputDir))
catch
{
Directory.Delete(outputDir, true);
Directory.Delete(workDir, true);
throw;
}
_fileSystem.MoveDirectory(workDir, outputDir);
return trickplayInfo;
}
private bool CanGenerateTrickplay(Video video, int interval)

View File

@@ -7,7 +7,6 @@ using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
@@ -50,7 +49,7 @@ internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine
foreach (var virtualFolder in virtualFolders)
{
var options = virtualFolder.LibraryOptions;
if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
if (options?.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
{
continue;
}

View File

@@ -515,7 +515,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
PlayCount = dto.GetInt32(4),
IsFavorite = dto.GetBoolean(5),
PlaybackPositionTicks = dto.GetInt64(6),
LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
LastPlayedDate = dto.IsDBNull(7) ? null : ReadDateTimeFromColumn(dto, 7),
AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
Likes = null,
@@ -524,6 +524,28 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
};
}
private static DateTime? ReadDateTimeFromColumn(SqliteDataReader reader, int index)
{
// Try reading as a formatted date string first (handles ISO-8601 dates).
if (reader.TryReadDateTime(index, out var dateTimeResult))
{
return dateTimeResult;
}
// Some databases have Unix epoch timestamps stored as integers.
// SqliteDataReader.GetDateTime interprets integers as Julian dates, which crashes
// for Unix epoch values. Handle them explicitly.
var rawValue = reader.GetValue(index);
if (rawValue is long unixTimestamp
&& unixTimestamp > 0
&& unixTimestamp <= DateTimeOffset.MaxValue.ToUnixTimeSeconds())
{
return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime;
}
return null;
}
private AncestorId GetAncestorId(SqliteDataReader reader)
{
return new AncestorId()

View File

@@ -162,7 +162,7 @@ public sealed class SetupServer : IDisposable
{
var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), config.EnableIPv4, config.EnableIPv6);
knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6);
var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
var bindInterfaces = NetworkManager.GetAllBindInterfaces(_loggerFactory.CreateLogger<NetworkManager>(), false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
bindInterfaces,
config.InternalHttpPort,

View File

@@ -154,11 +154,6 @@ namespace MediaBrowser.Controller.Entities.Audio
return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
}
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Music;

View File

@@ -1171,11 +1171,18 @@ namespace MediaBrowser.Controller.Entities
info.Video3DFormat = video.Video3DFormat;
info.Timestamp = video.Timestamp;
if (video.IsShortcut)
if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
{
info.IsRemote = true;
info.Path = video.ShortcutPath;
info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
// Only allow remote shortcut paths — local file paths in .strm files
// could be used to read arbitrary files from the server.
if (shortcutProtocol != MediaProtocol.File)
{
info.IsRemote = true;
info.Path = video.ShortcutPath;
info.Protocol = shortcutProtocol;
}
}
if (string.IsNullOrEmpty(info.Container))
@@ -1600,7 +1607,6 @@ namespace MediaBrowser.Controller.Entities
if (string.IsNullOrEmpty(rating))
{
Logger.LogDebug("{0} has no parental rating set.", Name);
return !GetBlockUnratedValue(user);
}

View File

@@ -21,6 +21,8 @@ namespace MediaBrowser.Controller.Entities
ExcludePersonTypes = excludePersonTypes;
}
public int? StartIndex { get; set; }
/// <summary>
/// Gets or sets the maximum number of items the query should return.
/// </summary>
@@ -28,6 +30,8 @@ namespace MediaBrowser.Controller.Entities
public Guid ItemId { get; set; }
public Guid? ParentId { get; set; }
public IReadOnlyList<string> PersonTypes { get; }
public IReadOnlyList<string> ExcludePersonTypes { get; }

View File

@@ -33,18 +33,18 @@ namespace MediaBrowser.Controller.MediaEncoding
public partial class EncodingHelper
{
/// <summary>
/// The codec validation regex.
/// The codec validation regex string.
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
/// <summary>
/// The level validation regex.
/// The level validation regex string.
/// This regular expression matches strings representing a double.
/// </summary>
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?";
private const string _defaultMjpegEncoder = "mjpeg";
@@ -85,8 +85,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
private static readonly string[] _videoProfilesH264 =
[
@@ -180,6 +179,22 @@ namespace MediaBrowser.Controller.MediaEncoding
RemoveHdr10Plus,
}
/// <summary>
/// The codec validation regex.
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
[GeneratedRegex(ContainerValidationRegexStr)]
public static partial Regex ContainerValidationRegex();
/// <summary>
/// The level validation regex string.
/// This regular expression matches strings representing a double.
/// </summary>
[GeneratedRegex(LevelValidationRegexStr)]
public static partial Regex LevelValidationRegex();
[GeneratedRegex(@"\s+")]
private static partial Regex WhiteSpaceRegex();
@@ -476,7 +491,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
if (_containerValidationRegex.IsMatch(codec))
if (ContainerValidationRegex().IsMatch(codec))
{
return codec.ToLowerInvariant();
}
@@ -517,7 +532,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container))
{
return null;
}
@@ -735,7 +750,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
if (!_containerValidationRegex.IsMatch(codec))
if (!ContainerValidationRegex().IsMatch(codec))
{
codec = "aac";
}
@@ -1566,14 +1581,15 @@ namespace MediaBrowser.Controller.MediaEncoding
int bitrate = state.OutputVideoBitrate.Value;
// Bit rate under 1000k is not allowed in h264_qsv
// Bit rate under 1000k is not allowed in h264_qsv.
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
bitrate = Math.Max(bitrate, 1000);
}
// Currently use the same buffer size for all encoders
int bufsize = bitrate * 2;
// Currently use the same buffer size for all non-QSV encoders.
// Use long arithmetic to prevent int32 overflow for very high bitrate values.
int bufsize = (int)Math.Min((long)bitrate * 2, int.MaxValue);
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
@@ -1603,7 +1619,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
// Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}");
// Use long arithmetic and clamp to int.MaxValue to prevent int32 overflow
// (e.g. bitrate * 4 wraps to a negative value for bitrates above ~537 million)
int qsvMaxrate = (int)Math.Min((long)bitrate + 1, int.MaxValue);
int qsvInitOcc = (int)Math.Min((long)bitrate * 2, int.MaxValue);
int qsvBufsize = (int)Math.Min((long)bitrate * 4, int.MaxValue);
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}");
}
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
@@ -1782,38 +1804,40 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
{
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
return null;
}
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.3 (15) and lower for maximum compatibility.
// https://en.wikipedia.org/wiki/AV1#Levels
if (requestLevel < 0 || requestLevel >= 15)
{
// Transcode to level 5.3 (15) and lower for maximum compatibility.
// https://en.wikipedia.org/wiki/AV1#Levels
if (requestLevel < 0 || requestLevel >= 15)
{
return "15";
}
return "15";
}
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
}
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel < 0 || requestLevel >= 150)
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel < 0 || requestLevel >= 150)
{
return "150";
}
return "150";
}
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
}
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (requestLevel < 0 || requestLevel >= 51)
{
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (requestLevel < 0 || requestLevel >= 51)
{
return "51";
}
return "51";
}
}
@@ -2203,12 +2227,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
var level = state.GetRequestedLevel(targetVideoCodec);
var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec));
if (!string.IsNullOrEmpty(level))
{
level = NormalizeTranscodingLevel(state, level);
// libx264, QSV, AMF can adjust the given level to match the output.
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
@@ -2606,8 +2628,16 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
// Cap the max target bitrate to intMax/2 to satisfy the bufsize=bitrate*2.
return Math.Min(bitrate ?? 0, int.MaxValue / 2);
// Cap the max target bitrate to 400 Mbps.
// No consumer or professional hardware transcode target exceeds this value
// (Intel QSV tops out at ~300 Mbps for H.264; HEVC High Tier Level 5.x is ~240 Mbps).
// Without this cap, plugin-provided MPEG-TS streams with no usable bitrate metadata
// can produce unreasonably large -bufsize/-maxrate values for the encoder.
// Note: the existing FallbackMaxStreamingBitrate mechanism (default 30 Mbps) only
// applies when a LiveStreamId is set (M3U/HDHR sources). Plugin streams and other
// sources that bypass the LiveTV pipeline are not covered by it.
const int MaxSaneBitrate = 400_000_000; // 400 Mbps
return Math.Min(bitrate ?? 0, MaxSaneBitrate);
}
private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
@@ -6373,17 +6403,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
&& ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false)
|| (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false)))
{
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
{
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
{
return null;
}
return null;
}
}
@@ -7226,8 +7254,10 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
}
int readrate = 0;
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
{
readrate = 1;
inputModifier += " -re";
}
else if (encodingOptions.EnableSegmentDeletion
@@ -7238,7 +7268,15 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
// to prevent ffmpeg from exiting prematurely (due to fast drive)
inputModifier += " -readrate 10";
readrate = 10;
inputModifier += $" -readrate {readrate}";
}
// Set a larger catchup value to revert to the old behavior,
// otherwise, remuxing might stall due to this new option
if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption)
{
inputModifier += $" -readrate_catchup {readrate * 100}";
}
var flags = new List<string>();

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -31,4 +32,13 @@ public interface IMediaSegmentProvider
/// <param name="item">The base item to extract segments from.</param>
/// <returns>True if item is supported, otherwise false.</returns>
ValueTask<bool> Supports(BaseItem item);
/// <summary>
/// Called when extracted segment data for an item is being pruned.
/// Providers should delete any cached analysis data they hold for the given item.
/// </summary>
/// <param name="itemId">The item whose data is being pruned.</param>
/// <param name="cancellationToken">Abort token.</param>
/// <returns>A task representing the asynchronous cleanup operation.</returns>
Task CleanupExtractedData(Guid itemId, CancellationToken cancellationToken);
}

View File

@@ -115,7 +115,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
await ExtractAllAttachmentsInternal(
inputFile,
mediaSource,
false,
cancellationToken).ConfigureAwait(false);
}
}
@@ -123,7 +122,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
private async Task ExtractAllAttachmentsInternal(
string inputFile,
MediaSourceInfo mediaSource,
bool isExternal,
CancellationToken cancellationToken)
{
var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
@@ -142,11 +140,19 @@ namespace MediaBrowser.MediaEncoding.Attachments
return;
}
// Files without video/audio streams (e.g. MKS subtitle files) don't need a dummy
// output since there are no streams to process. Omit "-t 0 -f null null" so ffmpeg
// doesn't fail trying to open an output with no streams. It will exit with code 1
// ("at least one output file must be specified") which is expected and harmless
// since we only need the -dump_attachment side effect.
var hasVideoOrAudioStream = mediaSource.MediaStreams
.Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
var processArgs = string.Format(
CultureInfo.InvariantCulture,
"-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
"-dump_attachment:t \"\" -y {0} -i {1} {2}",
inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
inputPath);
inputPath,
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
int exitCode;
@@ -185,12 +191,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
if (exitCode != 0)
{
if (isExternal && exitCode == 1)
{
// ffmpeg returns exitCode 1 because there is no video or audio stream
// this can be ignored
}
else
if (hasVideoOrAudioStream || exitCode != 1)
{
failed = true;
@@ -205,7 +206,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
}
}
}
else if (!Directory.Exists(outputFolder))
if (!failed && !Directory.Exists(outputFolder))
{
failed = true;
}
@@ -246,6 +248,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
{
await ExtractAttachmentInternal(
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
mediaSource,
mediaAttachment.Index,
attachmentPath,
cancellationToken).ConfigureAwait(false);
@@ -257,6 +260,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
private async Task ExtractAttachmentInternal(
string inputPath,
MediaSourceInfo mediaSource,
int attachmentStreamIndex,
string outputPath,
CancellationToken cancellationToken)
@@ -267,12 +271,15 @@ namespace MediaBrowser.MediaEncoding.Attachments
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
var hasVideoOrAudioStream = mediaSource.MediaStreams
.Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
var processArgs = string.Format(
CultureInfo.InvariantCulture,
"-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
"-dump_attachment:{1} \"{2}\" -i {0} {3}",
inputPath,
attachmentStreamIndex,
EncodingUtils.NormalizePath(outputPath));
EncodingUtils.NormalizePath(outputPath),
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
int exitCode;
@@ -310,22 +317,26 @@ namespace MediaBrowser.MediaEncoding.Attachments
if (exitCode != 0)
{
failed = true;
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
try
if (hasVideoOrAudioStream || exitCode != 1)
{
if (File.Exists(outputPath))
failed = true;
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
try
{
_fileSystem.DeleteFile(outputPath);
if (File.Exists(outputPath))
{
_fileSystem.DeleteFile(outputPath);
}
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
}
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
}
}
else if (!File.Exists(outputPath))
if (!failed && !File.Exists(outputPath))
{
failed = true;
}

View File

@@ -74,9 +74,9 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <param name="dict">The dict.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict)
private static Dictionary<string, string?> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string?> dict)
{
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
return new Dictionary<string, string?>(dict, StringComparer.OrdinalIgnoreCase);
}
}
}

View File

@@ -1,5 +1,3 @@
#nullable disable
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -22,21 +20,21 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The profile.</value>
[JsonPropertyName("profile")]
public string Profile { get; set; }
public string? Profile { get; set; }
/// <summary>
/// Gets or sets the codec_name.
/// </summary>
/// <value>The codec_name.</value>
[JsonPropertyName("codec_name")]
public string CodecName { get; set; }
public string? CodecName { get; set; }
/// <summary>
/// Gets or sets the codec_long_name.
/// </summary>
/// <value>The codec_long_name.</value>
[JsonPropertyName("codec_long_name")]
public string CodecLongName { get; set; }
public string? CodecLongName { get; set; }
/// <summary>
/// Gets or sets the codec_type.
@@ -50,49 +48,49 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The sample_rate.</value>
[JsonPropertyName("sample_rate")]
public string SampleRate { get; set; }
public string? SampleRate { get; set; }
/// <summary>
/// Gets or sets the channels.
/// </summary>
/// <value>The channels.</value>
[JsonPropertyName("channels")]
public int Channels { get; set; }
public int? Channels { get; set; }
/// <summary>
/// Gets or sets the channel_layout.
/// </summary>
/// <value>The channel_layout.</value>
[JsonPropertyName("channel_layout")]
public string ChannelLayout { get; set; }
public string? ChannelLayout { get; set; }
/// <summary>
/// Gets or sets the avg_frame_rate.
/// </summary>
/// <value>The avg_frame_rate.</value>
[JsonPropertyName("avg_frame_rate")]
public string AverageFrameRate { get; set; }
public string? AverageFrameRate { get; set; }
/// <summary>
/// Gets or sets the duration.
/// </summary>
/// <value>The duration.</value>
[JsonPropertyName("duration")]
public string Duration { get; set; }
public string? Duration { get; set; }
/// <summary>
/// Gets or sets the bit_rate.
/// </summary>
/// <value>The bit_rate.</value>
[JsonPropertyName("bit_rate")]
public string BitRate { get; set; }
public string? BitRate { get; set; }
/// <summary>
/// Gets or sets the width.
/// </summary>
/// <value>The width.</value>
[JsonPropertyName("width")]
public int Width { get; set; }
public int? Width { get; set; }
/// <summary>
/// Gets or sets the refs.
@@ -106,21 +104,21 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The height.</value>
[JsonPropertyName("height")]
public int Height { get; set; }
public int? Height { get; set; }
/// <summary>
/// Gets or sets the display_aspect_ratio.
/// </summary>
/// <value>The display_aspect_ratio.</value>
[JsonPropertyName("display_aspect_ratio")]
public string DisplayAspectRatio { get; set; }
public string? DisplayAspectRatio { get; set; }
/// <summary>
/// Gets or sets the tags.
/// </summary>
/// <value>The tags.</value>
[JsonPropertyName("tags")]
public IReadOnlyDictionary<string, string> Tags { get; set; }
public IReadOnlyDictionary<string, string?>? Tags { get; set; }
/// <summary>
/// Gets or sets the bits_per_sample.
@@ -141,7 +139,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The r_frame_rate.</value>
[JsonPropertyName("r_frame_rate")]
public string RFrameRate { get; set; }
public string? RFrameRate { get; set; }
/// <summary>
/// Gets or sets the has_b_frames.
@@ -155,70 +153,70 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The sample_aspect_ratio.</value>
[JsonPropertyName("sample_aspect_ratio")]
public string SampleAspectRatio { get; set; }
public string? SampleAspectRatio { get; set; }
/// <summary>
/// Gets or sets the pix_fmt.
/// </summary>
/// <value>The pix_fmt.</value>
[JsonPropertyName("pix_fmt")]
public string PixelFormat { get; set; }
public string? PixelFormat { get; set; }
/// <summary>
/// Gets or sets the level.
/// </summary>
/// <value>The level.</value>
[JsonPropertyName("level")]
public int Level { get; set; }
public int? Level { get; set; }
/// <summary>
/// Gets or sets the time_base.
/// </summary>
/// <value>The time_base.</value>
[JsonPropertyName("time_base")]
public string TimeBase { get; set; }
public string? TimeBase { get; set; }
/// <summary>
/// Gets or sets the start_time.
/// </summary>
/// <value>The start_time.</value>
[JsonPropertyName("start_time")]
public string StartTime { get; set; }
public string? StartTime { get; set; }
/// <summary>
/// Gets or sets the codec_time_base.
/// </summary>
/// <value>The codec_time_base.</value>
[JsonPropertyName("codec_time_base")]
public string CodecTimeBase { get; set; }
public string? CodecTimeBase { get; set; }
/// <summary>
/// Gets or sets the codec_tag.
/// </summary>
/// <value>The codec_tag.</value>
[JsonPropertyName("codec_tag")]
public string CodecTag { get; set; }
public string? CodecTag { get; set; }
/// <summary>
/// Gets or sets the codec_tag_string.
/// Gets or sets the codec_tag_string?.
/// </summary>
/// <value>The codec_tag_string.</value>
[JsonPropertyName("codec_tag_string")]
public string CodecTagString { get; set; }
/// <value>The codec_tag_string?.</value>
[JsonPropertyName("codec_tag_string?")]
public string? CodecTagString { get; set; }
/// <summary>
/// Gets or sets the sample_fmt.
/// </summary>
/// <value>The sample_fmt.</value>
[JsonPropertyName("sample_fmt")]
public string SampleFmt { get; set; }
public string? SampleFmt { get; set; }
/// <summary>
/// Gets or sets the dmix_mode.
/// </summary>
/// <value>The dmix_mode.</value>
[JsonPropertyName("dmix_mode")]
public string DmixMode { get; set; }
public string? DmixMode { get; set; }
/// <summary>
/// Gets or sets the start_pts.
@@ -232,90 +230,90 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <value>The is_avc.</value>
[JsonPropertyName("is_avc")]
public bool IsAvc { get; set; }
public bool? IsAvc { get; set; }
/// <summary>
/// Gets or sets the nal_length_size.
/// </summary>
/// <value>The nal_length_size.</value>
[JsonPropertyName("nal_length_size")]
public string NalLengthSize { get; set; }
public string? NalLengthSize { get; set; }
/// <summary>
/// Gets or sets the ltrt_cmixlev.
/// </summary>
/// <value>The ltrt_cmixlev.</value>
[JsonPropertyName("ltrt_cmixlev")]
public string LtrtCmixlev { get; set; }
public string? LtrtCmixlev { get; set; }
/// <summary>
/// Gets or sets the ltrt_surmixlev.
/// </summary>
/// <value>The ltrt_surmixlev.</value>
[JsonPropertyName("ltrt_surmixlev")]
public string LtrtSurmixlev { get; set; }
public string? LtrtSurmixlev { get; set; }
/// <summary>
/// Gets or sets the loro_cmixlev.
/// </summary>
/// <value>The loro_cmixlev.</value>
[JsonPropertyName("loro_cmixlev")]
public string LoroCmixlev { get; set; }
public string? LoroCmixlev { get; set; }
/// <summary>
/// Gets or sets the loro_surmixlev.
/// </summary>
/// <value>The loro_surmixlev.</value>
[JsonPropertyName("loro_surmixlev")]
public string LoroSurmixlev { get; set; }
public string? LoroSurmixlev { get; set; }
/// <summary>
/// Gets or sets the field_order.
/// </summary>
/// <value>The field_order.</value>
[JsonPropertyName("field_order")]
public string FieldOrder { get; set; }
public string? FieldOrder { get; set; }
/// <summary>
/// Gets or sets the disposition.
/// </summary>
/// <value>The disposition.</value>
[JsonPropertyName("disposition")]
public IReadOnlyDictionary<string, int> Disposition { get; set; }
public IReadOnlyDictionary<string, int>? Disposition { get; set; }
/// <summary>
/// Gets or sets the color range.
/// </summary>
/// <value>The color range.</value>
[JsonPropertyName("color_range")]
public string ColorRange { get; set; }
public string? ColorRange { get; set; }
/// <summary>
/// Gets or sets the color space.
/// </summary>
/// <value>The color space.</value>
[JsonPropertyName("color_space")]
public string ColorSpace { get; set; }
public string? ColorSpace { get; set; }
/// <summary>
/// Gets or sets the color transfer.
/// </summary>
/// <value>The color transfer.</value>
[JsonPropertyName("color_transfer")]
public string ColorTransfer { get; set; }
public string? ColorTransfer { get; set; }
/// <summary>
/// Gets or sets the color primaries.
/// </summary>
/// <value>The color primaries.</value>
[JsonPropertyName("color_primaries")]
public string ColorPrimaries { get; set; }
public string? ColorPrimaries { get; set; }
/// <summary>
/// Gets or sets the side_data_list.
/// </summary>
/// <value>The side_data_list.</value>
[JsonPropertyName("side_data_list")]
public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; }
public IReadOnlyList<MediaStreamInfoSideData>? SideDataList { get; set; }
}
}

View File

@@ -697,24 +697,18 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <returns>MediaStream.</returns>
private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList<MediaFrameInfo> frameInfoList)
{
// These are mp4 chapters
if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
{
// Edit: but these are also sometimes subtitles?
// return null;
}
var stream = new MediaStream
{
Codec = streamInfo.CodecName,
Profile = streamInfo.Profile,
Width = streamInfo.Width,
Height = streamInfo.Height,
Level = streamInfo.Level,
Index = streamInfo.Index,
PixelFormat = streamInfo.PixelFormat,
NalLengthSize = streamInfo.NalLengthSize,
TimeBase = streamInfo.TimeBase,
CodecTimeBase = streamInfo.CodecTimeBase,
IsAVC = streamInfo.IsAvc
CodecTimeBase = streamInfo.CodecTimeBase
};
// Filter out junk
@@ -774,10 +768,6 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
// Graphical subtitle may have width and height info
stream.Width = streamInfo.Width;
stream.Height = streamInfo.Height;
if (string.IsNullOrEmpty(stream.Title))
{
// mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
@@ -790,6 +780,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
else if (streamInfo.CodecType == CodecType.Video)
{
stream.IsAVC = streamInfo.IsAvc;
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
@@ -822,8 +813,6 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.Type = MediaStreamType.Video;
}
stream.Width = streamInfo.Width;
stream.Height = streamInfo.Height;
stream.AspectRatio = GetAspectRatio(streamInfo);
if (streamInfo.BitsPerSample > 0)
@@ -1091,8 +1080,8 @@ namespace MediaBrowser.MediaEncoding.Probing
&& width > 0
&& height > 0))
{
width = info.Width;
height = info.Height;
width = info.Width.Value;
height = info.Height.Value;
}
if (width > 0 && height > 0)

View File

@@ -577,7 +577,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var outputPaths = new List<string>();
var args = string.Format(
CultureInfo.InvariantCulture,
"-i {0} -copyts",
"-i {0}",
inputPath);
foreach (var subtitleStream in subtitleStreams)
@@ -602,7 +602,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
outputPaths.Add(outputPath);
args += string.Format(
CultureInfo.InvariantCulture,
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
streamIndex,
outputCodec,
outputPath);
@@ -621,7 +621,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var outputPaths = new List<string>();
var args = string.Format(
CultureInfo.InvariantCulture,
"-i {0} -copyts",
"-i {0}",
inputPath);
foreach (var subtitleStream in subtitleStreams)
@@ -647,7 +647,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
outputPaths.Add(outputPath);
args += string.Format(
CultureInfo.InvariantCulture,
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
streamIndex,
outputCodec,
outputPath);

View File

@@ -789,6 +789,12 @@ namespace MediaBrowser.Model.Dto
/// <value>The gain required for audio normalization.</value>
public float? NormalizationGain { get; set; }
/// <summary>
/// Gets or sets the gain required for audio normalization. This field is inherited from music album normalization gain.
/// </summary>
/// <value>The gain required for audio normalization.</value>
public float? AlbumNormalizationGain { get; set; }
/// <summary>
/// Gets or sets the current program.
/// </summary>

View File

@@ -2,7 +2,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;

View File

@@ -0,0 +1,23 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Books.Isbn
{
/// <inheritdoc />
public class IsbnExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => "ISBN";
/// <inheritdoc />
public string Key => "ISBN";
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Book;
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Providers.Books.Isbn;
/// <inheritdoc/>
public class IsbnExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => "ISBN";
/// <inheritdoc />
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
if (item.TryGetProviderId("ISBN", out var externalId))
{
if (item is Book)
{
yield return $"https://search.worldcat.org/search?q=bn:{externalId}";
}
}
}
}

View File

@@ -262,9 +262,28 @@ namespace MediaBrowser.Providers.MediaInfo
private void FetchShortcutInfo(BaseItem item)
{
item.ShortcutPath = File.ReadAllLines(item.Path)
var shortcutPath = File.ReadAllLines(item.Path)
.Select(NormalizeStrmLine)
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
if (string.IsNullOrWhiteSpace(shortcutPath))
{
return;
}
// Only allow remote URLs in .strm files to prevent local file access
if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri)
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)))
{
item.ShortcutPath = shortcutPath;
}
else
{
_logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath);
}
}
/// <summary>

View File

@@ -0,0 +1,23 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.ComicVine
{
/// <inheritdoc />
public class ComicVineExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => "Comic Vine";
/// <inheritdoc />
public string Key => "ComicVine";
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Book;
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Providers.Plugins.ComicVine;
/// <inheritdoc/>
public class ComicVineExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => "Comic Vine";
/// <inheritdoc />
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
if (item.TryGetProviderId("ComicVine", out var externalId))
{
switch (item)
{
case Person:
case Book:
yield return $"https://comicvine.gamespot.com/{externalId}";
break;
}
}
}
}

View File

@@ -0,0 +1,23 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.ComicVine
{
/// <inheritdoc />
public class ComicVinePersonExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => "Comic Vine";
/// <inheritdoc />
public string Key => "ComicVine";
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person;
}
}

View File

@@ -0,0 +1,23 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.GoogleBooks
{
/// <inheritdoc />
public class GoogleBooksExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => "Google Books";
/// <inheritdoc />
public string Key => "GoogleBooks";
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Book;
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Providers.Plugins.GoogleBooks;
/// <inheritdoc/>
public class GoogleBooksExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc />
public string Name => "Google Books";
/// <inheritdoc />
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
if (item.TryGetProviderId("GoogleBooks", out var externalId))
{
if (item is Book)
{
yield return $"https://books.google.com/books?id={externalId}";
}
}
}
}

View File

@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@@ -32,6 +33,7 @@ namespace MediaBrowser.Providers.Subtitles
private readonly ILibraryMonitor _monitor;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly ILocalizationManager _localization;
private readonly HashSet<string> _allowedSubtitleFormats;
private readonly ISubtitleProvider[] _subtitleProviders;
@@ -41,7 +43,8 @@ namespace MediaBrowser.Providers.Subtitles
ILibraryMonitor monitor,
IMediaSourceManager mediaSourceManager,
ILocalizationManager localizationManager,
IEnumerable<ISubtitleProvider> subtitleProviders)
IEnumerable<ISubtitleProvider> subtitleProviders,
NamingOptions namingOptions)
{
_logger = logger;
_fileSystem = fileSystem;
@@ -51,6 +54,9 @@ namespace MediaBrowser.Providers.Subtitles
_subtitleProviders = subtitleProviders
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
.ToArray();
_allowedSubtitleFormats = new HashSet<string>(
namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')),
StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -171,6 +177,12 @@ namespace MediaBrowser.Providers.Subtitles
/// <inheritdoc />
public Task UploadSubtitle(Video video, SubtitleResponse response)
{
var format = response.Format;
if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format))
{
throw new ArgumentException($"Unsupported subtitle format: '{format}'");
}
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
return TrySaveSubtitle(video, libraryOptions, response);
}
@@ -193,7 +205,13 @@ namespace MediaBrowser.Providers.Subtitles
}
var savePaths = new List<string>();
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
var language = response.Language.ToLowerInvariant();
if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0)
{
throw new ArgumentException("Language contains invalid characters.");
}
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language;
if (response.IsForced)
{
@@ -221,15 +239,22 @@ namespace MediaBrowser.Providers.Subtitles
private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension)
{
if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException($"Invalid subtitle format: {extension}");
}
List<Exception>? exs = null;
foreach (var savePath in savePaths)
{
var path = savePath + "." + extension;
var path = Path.GetFullPath(savePath + "." + extension);
try
{
if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)
|| path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar;
var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar;
if (path.StartsWith(containingFolder, StringComparison.Ordinal)
|| path.StartsWith(metadataFolder, StringComparison.Ordinal))
{
var fileExists = File.Exists(path);
var counter = 0;

View File

@@ -547,7 +547,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
}
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection))
if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
{
writer.WriteElementString("collectionnumber", tmdbCollection);
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());

View File

@@ -24,61 +24,29 @@ public class SkiaEncoder : IImageEncoder
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths;
private static readonly SKImageFilter _imageFilter;
private static readonly SKTypeface[] _typefaces;
private static readonly SKTypeface?[] _typefaces = InitializeTypefaces();
private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution(
new SKSizeI(3, 3),
[
0, -.1f, 0,
-.1f, 1.4f, -.1f,
0, -.1f, 0
],
1f,
0f,
new SKPointI(1, 1),
SKShaderTileMode.Clamp,
true);
/// <summary>
/// The default sampling options, equivalent to old high quality filter settings when upscaling.
/// </summary>
public static readonly SKSamplingOptions UpscaleSamplingOptions;
public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
/// <summary>
/// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
/// </summary>
public static readonly SKSamplingOptions DefaultSamplingOptions;
#pragma warning disable CA1810
static SkiaEncoder()
#pragma warning restore CA1810
{
var kernel = new[]
{
0, -.1f, 0,
-.1f, 1.4f, -.1f,
0, -.1f, 0,
};
var kernelSize = new SKSizeI(3, 3);
var kernelOffset = new SKPointI(1, 1);
_imageFilter = SKImageFilter.CreateMatrixConvolution(
kernelSize,
kernel,
1f,
0f,
kernelOffset,
SKShaderTileMode.Clamp,
true);
// Initialize the list of typefaces
// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F)
_typefaces =
[
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ''), // CJK Japanese
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
];
// use cubic for upscaling
UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
// use bilinear for everything else
DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
}
public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
@@ -132,7 +100,7 @@ public class SkiaEncoder : IImageEncoder
/// <summary>
/// Gets the default typeface to use.
/// </summary>
public static SKTypeface DefaultTypeFace => _typefaces.Last();
public static SKTypeface? DefaultTypeFace => _typefaces.Last();
/// <summary>
/// Check if the native lib is available.
@@ -152,6 +120,40 @@ public class SkiaEncoder : IImageEncoder
}
}
/// <summary>
/// Initialize the list of typefaces
/// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
/// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F).
/// </summary>
/// <returns>The list of typefaces.</returns>
private static SKTypeface?[] InitializeTypefaces()
{
int[] chars = [
'鸡', // CJK Simplified Chinese
'雞', // CJK Traditional Chinese
'', // CJK Japanese
'각', // CJK Korean
128169, // Emojis, 128169 is the Pile of Poo (💩) emoji
'ז', // Hebrew
'ي' // Arabic
];
var fonts = new List<SKTypeface>(chars.Length + 1);
foreach (var ch in chars)
{
var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch);
if (font is not null)
{
fonts.Add(font);
}
}
// Default font
fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)
?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a'));
return fonts.ToArray();
}
/// <summary>
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
/// </summary>
@@ -809,7 +811,7 @@ public class SkiaEncoder : IImageEncoder
{
foreach (var typeface in _typefaces)
{
if (typeface.ContainsGlyphs(c))
if (typeface is not null && typeface.ContainsGlyphs(c))
{
return typeface;
}

View File

@@ -156,6 +156,13 @@ namespace Jellyfin.LiveTv.IO
if (mediaSource.ReadAtNativeFramerate)
{
inputModifier += " -re";
// Set a larger catchup value to revert to the old behavior,
// otherwise, remuxing might stall due to this new option
if (_mediaEncoder.EncoderVersion >= new Version(8, 0))
{
inputModifier += " -readrate_catchup 100";
}
}
if (mediaSource.RequiresLooping)

View File

@@ -93,6 +93,13 @@ namespace Jellyfin.LiveTv.TunerHosts
}
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
if (!IsValidChannelUrl(trimmedLine))
{
_logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine);
extInf = string.Empty;
continue;
}
var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -247,6 +254,16 @@ namespace Jellyfin.LiveTv.TunerHosts
return numberString;
}
private static bool IsValidChannelUrl(string url)
{
return Uri.TryCreate(url, UriKind.Absolute, out var uri)
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase));
}
private static bool IsValidChannelNumber(string numberString)
{
if (string.IsNullOrWhiteSpace(numberString)

View File

@@ -747,12 +747,13 @@ public class NetworkManager : INetworkManager, IDisposable
/// <inheritdoc/>
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
{
return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled);
return NetworkManager.GetAllBindInterfaces(_logger, individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled);
}
/// <summary>
/// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound.
/// </summary>
/// <param name="logger">Logger to use for messages.</param>
/// <param name="individualInterfaces">Defines that only known interfaces should be used.</param>
/// <param name="configurationManager">The ConfigurationManager.</param>
/// <param name="knownInterfaces">The known interfaces that gets returned if possible or instructed.</param>
@@ -760,6 +761,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// <param name="readIpv6">Include IPV6 type interfaces.</param>
/// <returns>A list of ip address of which jellyfin should bind to.</returns>
public static IReadOnlyList<IPData> GetAllBindInterfaces(
ILogger<NetworkManager> logger,
bool individualInterfaces,
IConfigurationManager configurationManager,
IReadOnlyList<IPData> knownInterfaces,
@@ -773,6 +775,13 @@ public class NetworkManager : INetworkManager, IDisposable
return knownInterfaces;
}
// TODO: remove when upgrade to dotnet 11 is done
if (readIpv6 && !Socket.OSSupportsIPv6)
{
logger.LogWarning("IPv6 Unsupported by OS, not listening on IPv6");
readIpv6 = false;
}
// No bind address and no exclusions, so listen on all interfaces.
var result = new List<IPData>();
if (readIpv4 && readIpv6)
@@ -869,7 +878,20 @@ public class NetworkManager : INetworkManager, IDisposable
if (availableInterfaces.Count == 0)
{
// There isn't any others, so we'll use the loopback.
result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
// Prefer loopback address matching the source's address family
if (source is not null && source.AddressFamily == AddressFamily.InterNetwork && IsIPv4Enabled)
{
result = "127.0.0.1";
}
else if (source is not null && source.AddressFamily == AddressFamily.InterNetworkV6 && IsIPv6Enabled)
{
result = "::1";
}
else
{
result = IsIPv4Enabled ? "127.0.0.1" : "::1";
}
_logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
return result;
}
@@ -894,9 +916,19 @@ public class NetworkManager : INetworkManager, IDisposable
}
}
// Fallback to first available interface
// Fallback to an interface matching the source's address family, or first available
var preferredInterface = availableInterfaces
.FirstOrDefault(x => x.Address.AddressFamily == source.AddressFamily);
if (preferredInterface is not null)
{
result = NetworkUtils.FormatIPString(preferredInterface.Address);
_logger.LogDebug("{Source}: No matching subnet found, using interface with matching address family: {Result}", source, result);
return result;
}
result = NetworkUtils.FormatIPString(availableInterfaces[0].Address);
_logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
_logger.LogDebug("{Source}: No matching interfaces found, using first available interface as bind address: {Result}", source, result);
return result;
}

View File

@@ -209,8 +209,8 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("mkv,webm", res.Container);
Assert.Equal(2, res.MediaStreams.Count);
Assert.False(res.MediaStreams[0].IsAVC);
Assert.Equal(540, res.MediaStreams[0].Width);
Assert.Equal(360, res.MediaStreams[0].Height);
}
[Fact]

View File

@@ -29,6 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("[OCN] 720p-NEXT", " ")]
[InlineData("[tvN] .E01-E16.720p-NEXT", "")]
[InlineData("[tvN] E01~E16 END HDTV.H264.720p-WITH", " ")]
[InlineData("2026年01月10日23時00分00秒-[新]TRIGUN STARGAZE[].mp4", "20260110230000-[]TRIGUN STARGAZE")]
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
@@ -44,6 +45,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("American.Psycho.mkv")]
[InlineData("American Psycho.mkv")]
[InlineData("Run lola run (lola rennt) (2009).mp4")]
[InlineData("2026年01月05日00時55分00秒-[新].mp4")]
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
{
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out var newName));

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
@@ -269,8 +270,13 @@ namespace Jellyfin.Naming.Tests.Video
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].AlternateVersions);
Assert.Single(result);
Assert.Equal(6, result[0].AlternateVersions.Count);
// Verify 3D recognition is preserved on alternate versions
var hsbs = result[0].AlternateVersions.First(v => v.Path.Contains("3d-hsbs", StringComparison.Ordinal));
Assert.True(hsbs.Is3D);
Assert.Equal("hsbs", hsbs.Format3D);
}
[Fact]
@@ -435,5 +441,39 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Empty(result);
}
[Fact]
public void Resolve_GivenUnderscoreSeparator_GroupsVersions()
{
var files = new[]
{
"/movies/Movie (2020)/Movie (2020)_4K.mkv",
"/movies/Movie (2020)/Movie (2020)_1080p.mkv"
};
var result = VideoListResolver.Resolve(
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
}
[Fact]
public void Resolve_GivenDotSeparator_GroupsVersions()
{
var files = new[]
{
"/movies/Movie (2020)/Movie (2020).UHD.mkv",
"/movies/Movie (2020)/Movie (2020).1080p.mkv"
};
var result = VideoListResolver.Resolve(
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
}
}
}

View File

@@ -377,6 +377,8 @@ namespace Jellyfin.Networking.Tests
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "10.0.0.1", "192.168.1.209", "10.0.0.1")] // LAN not bound, so return external.
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "8.8.8.8", "10.0.0.1")] // return external bind address
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "192.168.1.210", "192.168.1.208")] // return LAN bind address
// Cross-subnet IPv4 request should return IPv4, not IPv6 (Issue #15898)
[InlineData("192.168.1.208/24,-16,eth16|fd00::1/64,10,eth7", "192.168.1.0/24", "", "192.168.2.100", "192.168.1.208")]
public void GetBindInterface_ValidSourceGiven_Success(string interfaces, string lan, string bind, string source, string result)
{
var conf = new NetworkConfiguration