Compare commits

...

65 Commits

Author SHA1 Message Date
JPVenson
bd224b031c Update README.md
Co-authored-by: theguymadmax <theguymadmax@proton.me>
2026-02-23 11:39:50 +01:00
JPVenson
21df053fe4 Revise note on hosting web client for development
Updated recommendation for hosting the web client separately during development.
2026-02-22 23:28:34 +01:00
INOUE Daisuke
56a469d8c3 Translated using Weblate (Japanese)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/
2026-02-21 02:41:32 +00:00
Bond-009
2dfebb51be Merge pull request #16266 from jellyfin/renovate/ci-deps
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Update github/codeql-action action to v4.32.4
2026-02-20 18:09:58 +01:00
renovate[bot]
716f4c8198 Update github/codeql-action action to v4.32.4 2026-02-20 15:41:35 +00:00
Andrew Rabert
9da046abc1 Merge pull request #16263 from jellyfin/fix/replace-pull-request-target
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 (windows-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Mitigate pull_request_target privilege escalation
2026-02-19 23:59:07 -05:00
Andrew Rabert
01eb56f047 Mitigate pull_request_target privilege escalation
Hotfix — replaces pull_request_target with pull_request to stop
granting write permissions and secrets to fork PRs. Some workflows
will break; can be fixed properly later.
2026-02-19 23:53:48 -05:00
Niels van Velzen
6829794aa0 Merge pull request #16104 from jellyfin/renovate/tmdblib-3.x
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
Project Automation / Project board (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 / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Update dependency TMDbLib to v3
2026-02-18 21:05:14 +01:00
Bond-009
06a6c6e16b Merge pull request #16249 from jellyfin/renovate/ci-deps
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
2026-02-17 18:39:50 +01:00
renovate[bot]
daf88a5ca2 Update actions/stale action to v10.2.0 2026-02-17 06:49:00 +00:00
Bond_009
b346370dfc Fix build 2026-02-15 11:28:42 +01:00
Bond-009
fc6419685c Merge pull request #16242 from jellyfin/renovate/coverlet.collector-8.x
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
Update dependency coverlet.collector to v8
2026-02-15 10:58:53 +01:00
renovate[bot]
04ffbe5e9a Update dependency TMDbLib to v3 2026-02-15 09:49:18 +00:00
Bond-009
d0809ce58b Merge pull request #16236 from theguymadmax/fix-season-unknown
Fix episodes appearing in Season Unknown incorrectly and prevent unnecessary virtual season creation
2026-02-15 10:48:55 +01:00
renovate[bot]
a56aa2dd53 Update dependency coverlet.collector to v8 2026-02-15 02:37:05 +00:00
Bond-009
1311f66f72 Merge pull request #16144 from DerMaddis/series-production-year
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
TmdbSeriesProvider: Set ProductionYear field
2026-02-14 12:35:54 +01:00
Bond-009
217068eeb7 Merge pull request #16235 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.32.3
2026-02-14 12:19:28 +01:00
Bond-009
44d55f7fa3 Merge pull request #15138 from kevgrig/issue15137
Add moveToTop option to IPlaylistManager.AddItemToPlaylistAsync
2026-02-14 12:08:00 +01:00
Bond-009
29582ed461 Merge branch 'master' into issue15137 2026-02-14 12:07:30 +01:00
Bond-009
ca6d499680 Update Jellyfin.Api/Controllers/PlaylistsController.cs 2026-02-14 12:06:18 +01:00
Bond-009
3b69859867 Merge pull request #14709 from loop95/fix-artist-kairon-irse
Fix: Add 'Kairon; IRSE!' to artist whitelist
2026-02-14 12:03:16 +01:00
Bond-009
c1c7de6a81 Merge pull request #16214 from jellyfin/renovate/asynckeyedlock-8.x
Update dependency AsyncKeyedLock to 8.0.2
2026-02-14 12:02:05 +01:00
Bond-009
7bcbe20641 Merge pull request #16217 from jellyfin/renovate/microsoft
Update Microsoft to 10.0.3
2026-02-14 11:58:34 +01:00
Bond-009
2edf23bb40 Merge pull request #16216 from jellyfin/renovate/dotnet-monorepo
Update dependency dotnet-ef to v10.0.3
2026-02-14 11:58:23 +01:00
theguymadmax
074aa7e639 Backport pull request #16231 from jellyfin/release-10.11.z
Skip image checks for empty folders

Original-merge: 8cd3090cee

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-02-14 05:57:26 -05:00
dfederm
a37e83d448 Backport pull request #16227 from jellyfin/release-10.11.z
Reattach user data after item removal during library scan

Original-merge: be71295693

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-02-14 05:57:25 -05:00
dfederm
d8543351e2 Backport pull request #16226 from jellyfin/release-10.11.z
Deduplicate provider IDs during MigrateLibraryDb migration

Original-merge: 58c330b63d

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-02-14 05:57:24 -05:00
saltpi
dce91cf8c8 Backport pull request #16116 from jellyfin/release-10.11.z
Fix TMDB image URLs missing size parameter

Original-merge: caa05c1bf2

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

Backported-by: Bond_009 <bond.009@outlook.com>
2026-02-14 05:57:23 -05:00
theguymadmax
48e456903e Apply review feedback 2026-02-13 16:28:22 -05:00
theguymadmax
2757c18312 Fix episodes appearing in Season Unknown incorrectly and prevent unnecessary virtual season creation 2026-02-13 11:52:10 -05:00
renovate[bot]
a1117a1fbd Update github/codeql-action action to v4.32.3 2026-02-13 15:33:57 +00:00
DerMaddis
f685a65241 add to CONTRIBUTORS.md 2026-02-13 12:11:13 +01:00
DerMaddis
e73ebc9741 TmdbSeriesProvider: Set ProductionYear in SearchResult mappers 2026-02-13 12:03:58 +01:00
renovate[bot]
18a1cd388a Update dependency AsyncKeyedLock to 8.0.2 2026-02-12 12:13:58 +00:00
renovate[bot]
6fff4a7bfa Update Microsoft to 10.0.3 2026-02-10 23:05:25 +00:00
renovate[bot]
5aad260767 Update dependency dotnet-ef to v10.0.3 2026-02-10 23:05:17 +00:00
Niels van Velzen
8b5914001d Merge pull request #16202 from JPVenson/feat/devcontainerupdate
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
Project Automation / Project board (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 / 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
Fix container and updated
2026-02-10 17:53:38 +01:00
Pavel Miniutka
5eaaad660d Translated using Weblate (Belarusian)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/
2026-02-10 09:51:30 +00:00
francescbassas
de36952f53 Translated using Weblate (Catalan)
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/ca/
2026-02-09 23:14:52 +00:00
Vincenzo Reale
6b8400cc3d Translated using Weblate (Italian)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-02-09 11:37:24 +00:00
Vincenzo Reale
a0bf5199ba Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-02-09 10:57:22 +00:00
Vincenzo Reale
01264c10a6 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-02-09 10:11:24 +00:00
Vincenzo Reale
558b31f386 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-02-09 10:10:39 +00:00
renovate[bot]
5656df4339 Update dependency z440.atl.core to 7.11.0 (#16199)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 23:45:38 +01:00
JPVenson
fa4d51c5e6 Fix container and updated 2026-02-07 18:50:08 +00:00
renovate[bot]
21bb702fd3 Update github/codeql-action action to v4.32.2 (#16171)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 10:30:03 -07:00
renovate[bot]
4a494271dd Update dependency AsyncKeyedLock to 8.0.1 (#16096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 10:29:54 -07:00
renovate[bot]
1f6768178a Update dependency Svg.Skia to 3.4.1 (#15941)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 10:27:55 -07:00
Milo Ivir
fead4acae1 Translated using Weblate (Croatian)
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/hr/
2026-02-06 22:08:06 +00:00
Milo Ivir
ccd042750d Translated using Weblate (Croatian)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/
2026-02-06 01:52:08 +00:00
Bond-009
e4619556ba Merge pull request #16181 from theguymadmax/trim-people
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
Project Automation / Project board (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
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Trim names and roles for people
2026-02-05 17:45:45 +01:00
Bond-009
9f2dc178f5 Merge pull request #16178 from theguymadmax/fix-invaild-viewtype
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Skip validation for empty landing preferences
2026-02-04 19:23:38 +01:00
theguymadmax
4c751e0a86 Normalize names and roles 2026-02-03 17:41:01 -05:00
Blackspirits
32d8086121 Translated using Weblate (Portuguese)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-02-03 11:05:55 +00:00
Blackspirits
7c200899d7 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-02-03 11:05:55 +00:00
theguymadmax
613d72fa26 Skip empty ViewType validation 2026-02-03 01:16:25 -05:00
Bond-009
909e2142d6 Merge pull request #14927 from nileshp87/patch-1
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
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
Add curly brace and parentheses support for parsing attribute values
2026-02-02 20:54:06 +01:00
Bond-009
8083ab78b5 Fix tests 2026-02-02 20:46:28 +01:00
Bond-009
cbc0138507 Merge pull request #16151 from Daydreamer-riri/tmdb-lang
Fix TMDB language handling: support full ISO 639-1 + ISO 3166-1 codes (e.g. zh-CN, zh-TW)
2026-02-02 20:29:03 +01:00
Riri
7b10888c95 Remove handling for 5-letter language codes in TMDb language normalization 2026-02-01 15:33:07 +08:00
DerMaddis
0a1dd56af6 TmdbSeriesProvider: Set ProductionYear field 2026-01-30 14:24:11 +01:00
Nilesh Patel
acb9da6f93 Add curly brace and parentheses support for parsing attribute values from paths 2025-12-10 12:23:05 -08:00
Kevin G
79061f4635 Change moveToTop in AddItemToPlaylistAsync to 0-based position
Signed-off-by: Kevin G <kevin@myplaceonline.com>
2025-10-23 19:27:34 -05:00
Kevin G
cd9154f110 Add moveToTop option to IPlaylistManager.AddItemToPlaylistAsync
Signed-off-by: Kevin G <kevin@myplaceonline.com>
2025-10-22 22:17:28 -05:00
loop
2b6febc8da Fix: Add 'Kairon; IRSE!' to artist whitelist 2025-08-25 10:41:21 +02:00
47 changed files with 434 additions and 231 deletions

View File

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

View File

@@ -1,17 +1,31 @@
{ {
"name": "Development Jellyfin Server", "name": "Development Jellyfin Server",
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", "image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble",
"service": "app", "service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
// reads the extensions list and installs them // The previous way of installing extensions via the vs command dont work on selfhosted devcontainers
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension", "customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit",
"alexcvzz.vscode-sqlite",
"streetsidesoftware.code-spell-checker",
"eamodio.gitlens",
"redhat.vscode-xml"
]
}
},
"features": { "features": {
"ghcr.io/devcontainers/features/dotnet:2": { "ghcr.io/devcontainers/features/dotnet:2": {
"version": "none", "version": "none",
"dotnetRuntimeVersions": "9.0", "dotnetRuntimeVersions": "10.0",
"aspNetCoreRuntimeVersions": "9.0" "aspNetCoreRuntimeVersions": "10.0"
}, },
"ghcr.io/devcontainers-extra/features/apt-packages:1": { "ghcr.io/devcontainers-extra/features/apt-packages:1": {
"preserve_apt_list": false, "preserve_apt_list": false,

View File

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

View File

@@ -1,6 +1,6 @@
name: ABI Compatibility name: ABI Compatibility
on: on:
pull_request_target: pull_request:
permissions: {} permissions: {}
@@ -77,7 +77,7 @@ jobs:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment) pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
name: ABI - Difference name: ABI - Difference
if: ${{ github.event_name == 'pull_request_target' }} if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- abi-head - abi-head

View File

@@ -5,7 +5,7 @@ on:
- master - master
tags: tags:
- 'v*' - 'v*'
pull_request_target: pull_request:
permissions: {} permissions: {}
@@ -78,7 +78,7 @@ jobs:
pull-requests: write pull-requests: write
name: OpenAPI - Difference name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request_target' }} if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- openapi-head - openapi-head
@@ -109,7 +109,7 @@ jobs:
publish-unstable: publish-unstable:
name: OpenAPI - Publish Unstable Spec name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- openapi-head - openapi-head

View File

@@ -4,7 +4,7 @@ on:
types: types:
- created - created
- edited - edited
pull_request_target: pull_request:
types: types:
- labeled - labeled
- synchronize - synchronize

View File

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

View File

@@ -4,7 +4,7 @@ on:
push: push:
branches: branches:
- master - master
pull_request_target: pull_request:
issue_comment: issue_comment:
permissions: {} permissions: {}

View File

@@ -4,7 +4,7 @@ on:
push: push:
branches: branches:
- master - master
pull_request_target: pull_request:
issue_comment: issue_comment:
permissions: {} permissions: {}
@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Apply label - name: Apply label
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}}
with: with:
dirtyLabel: 'merge conflict' dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'

View File

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

View File

@@ -287,3 +287,4 @@
- [Martin Reuter](https://github.com/reuterma24) - [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro) - [Michael McElroy](https://github.com/mcmcelro)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy) - [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
- [DerMaddis](https://github.com/dermaddis)

View File

@@ -4,7 +4,7 @@
</PropertyGroup> </PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies"> <ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="8.0.0" /> <PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -13,7 +13,7 @@
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" /> <PackageVersion Include="coverlet.collector" Version="8.0.0" />
<PackageVersion Include="Diacritics" Version="4.1.4" /> <PackageVersion Include="Diacritics" Version="4.1.4" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
@@ -26,27 +26,27 @@
<PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" />
@@ -74,13 +74,13 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" /> <PackageVersion Include="Svg.Skia" Version="3.4.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" />
<PackageVersion Include="System.Text.Json" Version="10.0.2" /> <PackageVersion Include="System.Text.Json" Version="10.0.3" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.10.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" /> <PackageVersion Include="TMDbLib" Version="3.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />

View File

@@ -18,7 +18,7 @@ public static class TvParserHelpers
/// <param name="status">The status string.</param> /// <param name="status">The status string.</param>
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param> /// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
/// <returns>Returns true if parsing was successful.</returns> /// <returns>Returns true if parsing was successful.</returns>
public static bool TryParseSeriesStatus(string status, out SeriesStatus? enumValue) public static bool TryParseSeriesStatus(string? status, out SeriesStatus? enumValue)
{ {
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus)) if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
{ {

View File

@@ -267,22 +267,24 @@ namespace Emby.Server.Implementations.Images
{ {
var image = item.GetImageInfo(type, 0); var image = item.GetImageInfo(type, 0);
if (image is not null) if (image is null)
{ {
if (!image.IsLocalFile) return GetItemsWithImages(item).Count is not 0;
{ }
return false;
}
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) if (!image.IsLocalFile)
{ {
return false; return false;
} }
if (!HasChangedByDate(item, image)) if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
{ {
return false; return false;
} }
if (!HasChangedByDate(item, image))
{
return false;
} }
return true; return true;

View File

@@ -37,15 +37,25 @@ namespace Emby.Server.Implementations.Library
while (attributeIndex > -1 && attributeIndex < maxIndex) while (attributeIndex > -1 && attributeIndex < maxIndex)
{ {
var attributeEnd = attributeIndex + attribute.Length; var attributeEnd = attributeIndex + attribute.Length;
if (attributeIndex > 0 if (attributeIndex > 0)
&& str[attributeIndex - 1] == '['
&& (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{ {
var closingIndex = str[attributeEnd..].IndexOf(']'); var attributeOpener = str[attributeIndex - 1];
// Must be at least 1 character before the closing bracket. var attributeCloser = attributeOpener switch
if (closingIndex > 1)
{ {
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString(); '[' => ']',
'(' => ')',
'{' => '}',
_ => '\0'
};
if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{
var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
{
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
}
} }
} }

View File

@@ -3,7 +3,7 @@
"Playlists": "Плэй-лісты", "Playlists": "Плэй-лісты",
"Latest": "Апошняе", "Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}", "LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} даданы ў бібліятэку", "ItemAddedWithName": "{0} дададзены ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены", "MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана", "NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны", "PluginInstalledWithName": "{0} быў усталяваны",
@@ -14,7 +14,7 @@
"Channels": "Каналы", "Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}", "ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі", "Collections": "Калекцыі",
"Default": а змаўчанні", "Default": радвызначана",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі", "Folders": "Папкі",
"Favorites": "Абранае", "Favorites": "Абранае",
@@ -81,8 +81,8 @@
"NotificationOptionInstallationFailed": "Збой усталёўкі", "NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.", "NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана", "NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена", "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося", "NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт", "NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна", "NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены", "NotificationOptionPluginUninstalled": "Плагін выдалены",

View File

@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.", "TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres", "TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.", "TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneig de les mediateques", "TaskRefreshLibrary": "Escaneja la mediateca",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.", "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols", "TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.", "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",

View File

@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}", "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}", "ChapterNameValue": "Poglavlje {0}",
"Collections": "Kolekcije", "Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinuo vezu", "DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}", "FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto", "ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti", "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Serije", "Shows": "Emisije",
"Songs": "Pjesme", "Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}", "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",

View File

@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Dispositivo: {1}", "AppDeviceValues": "App: {0}, Dispositivo: {1}",
"Application": "Applicazione", "Application": "Applicazione",
"Artists": "Artisti", "Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{0} autenticato con successo", "AuthenticationSucceededWithUserName": "{0} autenticato correttamente",
"Books": "Libri", "Books": "Libri",
"CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}", "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
"Channels": "Canali", "Channels": "Canali",
@@ -11,36 +11,36 @@
"Collections": "Collezioni", "Collections": "Collezioni",
"DeviceOfflineWithName": "{0} si è disconnesso", "DeviceOfflineWithName": "{0} si è disconnesso",
"DeviceOnlineWithName": "{0} è connesso", "DeviceOnlineWithName": "{0} è connesso",
"FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}", "FailedLoginAttemptWithUserName": "Tentativo di accesso non riuscito da {0}",
"Favorites": "Preferiti", "Favorites": "Preferiti",
"Folders": "Cartelle", "Folders": "Cartelle",
"Genres": "Generi", "Genres": "Generi",
"HeaderAlbumArtists": "Artisti dell'album", "HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare", "HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteAlbums": "Album preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti", "HeaderFavoriteArtists": "Artisti preferiti",
"HeaderFavoriteEpisodes": "Episodi Preferiti", "HeaderFavoriteEpisodes": "Episodi preferiti",
"HeaderFavoriteShows": "Serie TV Preferite", "HeaderFavoriteShows": "Serie TV preferite",
"HeaderFavoriteSongs": "Brani Preferiti", "HeaderFavoriteSongs": "Brani preferiti",
"HeaderLiveTV": "Diretta TV", "HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo", "HeaderNextUp": "Prossimo",
"HeaderRecordingGroups": "Gruppi di Registrazione", "HeaderRecordingGroups": "Gruppi di registrazione",
"HomeVideos": "Video Personali", "HomeVideos": "Video personali",
"Inherit": "Eredita", "Inherit": "Eredita",
"ItemAddedWithName": "{0} è stato aggiunto alla libreria", "ItemAddedWithName": "{0} è stato aggiunto alla libreria",
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria", "ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
"LabelIpAddressValue": "Indirizzo IP: {0}", "LabelIpAddressValue": "Indirizzo IP: {0}",
"LabelRunningTimeValue": "Durata: {0}", "LabelRunningTimeValue": "Durata: {0}",
"Latest": "Novità", "Latest": "Novità",
"MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato", "MessageApplicationUpdated": "Jellyfin Server è stato aggiornato",
"MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}", "MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata", "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata", "MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
"MixedContent": "Contenuto misto", "MixedContent": "Contenuto misto",
"Movies": "Film", "Movies": "Film",
"Music": "Musica", "Music": "Musica",
"MusicVideos": "Video Musicali", "MusicVideos": "Video musicali",
"NameInstallFailed": "{0} installazione fallita", "NameInstallFailed": "{0} installazione non riuscita",
"NameSeasonNumber": "Stagione {0}", "NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuta", "NameSeasonUnknown": "Stagione sconosciuta",
"NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.", "NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.",
@@ -49,37 +49,37 @@
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata", "NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
"NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta", "NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta",
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata", "NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
"NotificationOptionInstallationFailed": "Installazione fallita", "NotificationOptionInstallationFailed": "Installazione non riuscita",
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto", "NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
"NotificationOptionPluginError": "Errore del plugin", "NotificationOptionPluginError": "Errore del plugin",
"NotificationOptionPluginInstalled": "Plugin installato", "NotificationOptionPluginInstalled": "Plugin installato",
"NotificationOptionPluginUninstalled": "Plugin disinstallato", "NotificationOptionPluginUninstalled": "Plugin disinstallato",
"NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato", "NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
"NotificationOptionServerRestartRequired": "Riavvio del server necessario", "NotificationOptionServerRestartRequired": "Riavvio del server necessario",
"NotificationOptionTaskFailed": "Operazione pianificata fallita", "NotificationOptionTaskFailed": "Operazione pianificata non riuscita",
"NotificationOptionUserLockedOut": "Utente bloccato", "NotificationOptionUserLockedOut": "Utente bloccato",
"NotificationOptionVideoPlayback": "Riproduzione video iniziata", "NotificationOptionVideoPlayback": "Riproduzione video iniziata",
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta", "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto", "Photos": "Foto",
"Playlists": "Playlist", "Playlists": "Scalette",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} è stato Installato", "PluginInstalledWithName": "{0} è stato installato",
"PluginUninstalledWithName": "{0} è stato disinstallato", "PluginUninstalledWithName": "{0} è stato disinstallato",
"PluginUpdatedWithName": "{0} è stato aggiornato", "PluginUpdatedWithName": "{0} è stato aggiornato",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} fallito", "ScheduledTaskFailedWithName": "{0} non riuscito",
"ScheduledTaskStartedWithName": "{0} avviato", "ScheduledTaskStartedWithName": "{0} avviato",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato", "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Serie TV", "Shows": "Serie TV",
"Songs": "Brani", "Songs": "Brani",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", "StartupEmbyServerIsLoading": "Jellyfin Server si sta avviando. Riprova più tardi.",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}", "SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
"Sync": "Sincronizza", "Sync": "Sincronizza",
"System": "Sistema", "System": "Sistema",
"TvShows": "Serie TV", "TvShows": "Serie TV",
"User": "Utente", "User": "Utente",
"UserCreatedWithName": "L'utente {0} è stato creato", "UserCreatedWithName": "L'utente {0} è stato creato",
"UserDeletedWithName": "L'utente {0} è stato rimosso", "UserDeletedWithName": "L'utente {0} è stato eliminato",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}", "UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato", "UserLockedOutWithName": "L'utente {0} è stato bloccato",
"UserOfflineFromDevice": "{0} si è disconnesso da {1}", "UserOfflineFromDevice": "{0} si è disconnesso da {1}",
@@ -114,20 +114,20 @@
"TasksLibraryCategory": "Libreria", "TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione", "TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate", "TaskCleanActivityLog": "Attività di Registro Completate",
"TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie delletà configurata.", "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell'età configurata.",
"Undefined": "Non Definito", "Undefined": "Non specificato",
"Forced": "Forzato", "Forced": "Forzato",
"Default": "Predefinito", "Default": "Predefinito",
"TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.", "TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
"TaskOptimizeDatabase": "Ottimizza database", "TaskOptimizeDatabase": "Ottimizza database",
"TaskKeyframeExtractor": "Estrattore di Keyframe", "TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.", "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori scalette HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno", "External": "Esterno",
"HearingImpaired": "Non Udenti", "HearingImpaired": "Non udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay", "TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.", "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
"TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist", "TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette",
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.", "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.",
"TaskAudioNormalization": "Normalizzazione dell'audio", "TaskAudioNormalization": "Normalizzazione dell'audio",
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.", "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni", "TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",

View File

@@ -43,32 +43,32 @@
"NameInstallFailed": "{0}のインストールに失敗しました", "NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}", "NameSeasonNumber": "シーズン {0}",
"NameSeasonUnknown": "シーズン不明", "NameSeasonUnknown": "シーズン不明",
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。", "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロードできます。",
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります", "NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です", "NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
"NotificationOptionAudioPlayback": "オーディオの再生を開始", "NotificationOptionAudioPlayback": "オーディオの再生を開始",
"NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました", "NotificationOptionAudioPlaybackStopped": "オーディオの再生を停止",
"NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました", "NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました",
"NotificationOptionInstallationFailed": "インストール失敗", "NotificationOptionInstallationFailed": "インストール失敗",
"NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました", "NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました",
"NotificationOptionPluginError": "プラグインに障害が発生しました", "NotificationOptionPluginError": "プラグインに障害が発生しました",
"NotificationOptionPluginInstalled": "プラグインインストールされました", "NotificationOptionPluginInstalled": "プラグインインストールました",
"NotificationOptionPluginUninstalled": "プラグインアンインストールされました", "NotificationOptionPluginUninstalled": "プラグインアンインストールました",
"NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました", "NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました",
"NotificationOptionServerRestartRequired": "サーバーを再起動してください", "NotificationOptionServerRestartRequired": "サーバーを再起動してください",
"NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗", "NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗",
"NotificationOptionUserLockedOut": "ユーザーはロックされています", "NotificationOptionUserLockedOut": "ユーザーはロックされています",
"NotificationOptionVideoPlayback": "ビデオの再生を開始しました", "NotificationOptionVideoPlayback": "ビデオの再生を開始",
"NotificationOptionVideoPlaybackStopped": "ビデオを停止しました", "NotificationOptionVideoPlaybackStopped": "ビデオの再生を停止",
"Photos": "フォト", "Photos": "フォト",
"Playlists": "プレイリスト", "Playlists": "プレイリスト",
"Plugin": "プラグイン", "Plugin": "プラグイン",
"PluginInstalledWithName": "{0} インストールされました", "PluginInstalledWithName": "{0} インストールました",
"PluginUninstalledWithName": "{0} アンインストールされました", "PluginUninstalledWithName": "{0} アンインストールました",
"PluginUpdatedWithName": "{0} 更新されました", "PluginUpdatedWithName": "{0} 更新ました",
"ProviderValue": "プロバイダ: {0}", "ProviderValue": "プロバイダ: {0}",
"ScheduledTaskFailedWithName": "{0} が失敗しました", "ScheduledTaskFailedWithName": "{0} が失敗しました",
"ScheduledTaskStartedWithName": "{0} 開始されました", "ScheduledTaskStartedWithName": "{0} 開始",
"ServerNameNeedsToBeRestarted": "{0} を再起動してください", "ServerNameNeedsToBeRestarted": "{0} を再起動してください",
"Shows": "番組", "Shows": "番組",
"Songs": "曲", "Songs": "曲",

View File

@@ -124,8 +124,8 @@
"TaskKeyframeExtractor": "Extrator de Quadros-chave", "TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo", "External": "Externo",
"HearingImpaired": "Surdo", "HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay", "TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.", "TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -124,8 +124,8 @@
"HearingImpaired": "Problemas auditivos", "HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.", "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo", "TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.", "TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -198,17 +198,22 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(items, user, options); return Playlist.GetPlaylistItems(items, user, options);
} }
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId)
{ {
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) return AddToPlaylistInternal(
{ playlistId,
EnableImages = true itemIds,
}); user,
new DtoOptions(false)
{
EnableImages = true
},
position);
} }
private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options) private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options, int? position = null)
{ {
// Retrieve the existing playlist // Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist
@@ -243,7 +248,30 @@ namespace Emby.Server.Implementations.Playlists
} }
// Update the playlist in the repository // Update the playlist in the repository
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd]; if (position.HasValue)
{
if (position.Value <= 0)
{
playlist.LinkedChildren = [.. childrenToAdd, .. playlist.LinkedChildren];
}
else if (position.Value >= playlist.LinkedChildren.Length)
{
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
}
else
{
playlist.LinkedChildren = [
.. playlist.LinkedChildren[0..position.Value],
.. childrenToAdd,
.. playlist.LinkedChildren[position.Value..playlist.LinkedChildren.Length]
];
}
}
else
{
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
}
playlist.DateLastMediaAdded = DateTime.UtcNow; playlist.DateLastMediaAdded = DateTime.UtcNow;
await UpdatePlaylistInternal(playlist).ConfigureAwait(false); await UpdatePlaylistInternal(playlist).ConfigureAwait(false);

View File

@@ -191,9 +191,17 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{ {
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _)) var viewType = displayPreferences.CustomPrefs[key];
if (string.IsNullOrEmpty(viewType))
{ {
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]); displayPreferences.CustomPrefs.Remove(key);
continue;
}
if (!Enum.TryParse<ViewType>(viewType, true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", viewType);
displayPreferences.CustomPrefs.Remove(key); displayPreferences.CustomPrefs.Remove(key);
} }
} }

View File

@@ -359,6 +359,7 @@ public class PlaylistsController : BaseJellyfinApiController
/// </summary> /// </summary>
/// <param name="playlistId">The playlist id.</param> /// <param name="playlistId">The playlist id.</param>
/// <param name="ids">Item id, comma delimited.</param> /// <param name="ids">Item id, comma delimited.</param>
/// <param name="position">Optional. 0-based index where to place the items or at the end if <c>null</c>.</param>
/// <param name="userId">The userId.</param> /// <param name="userId">The userId.</param>
/// <response code="204">Items added to playlist.</response> /// <response code="204">Items added to playlist.</response>
/// <response code="403">Access forbidden.</response> /// <response code="403">Access forbidden.</response>
@@ -371,6 +372,7 @@ public class PlaylistsController : BaseJellyfinApiController
public async Task<ActionResult> AddItemToPlaylist( public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery] int? position,
[FromQuery] Guid? userId) [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
@@ -388,7 +390,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid(); return Forbid();
} }
await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, position, userId.Value).ConfigureAwait(false);
return NoContent(); return NoContent();
} }

View File

@@ -74,9 +74,10 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
/// <inheritdoc /> /// <inheritdoc />
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people) public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
{ {
foreach (var item in people.Where(e => e.Role is null)) foreach (var person in people)
{ {
item.Role = string.Empty; person.Name = person.Name.Trim();
person.Role = person.Role?.Trim() ?? string.Empty;
} }
// multiple metadata providers can provide the _same_ person // multiple metadata providers can provide the _same_ person

View File

@@ -1163,7 +1163,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Item = null!, Item = null!,
ProviderId = e[0], ProviderId = e[0],
ProviderValue = string.Join('|', e.Skip(1)) ProviderValue = string.Join('|', e.Skip(1))
}).ToArray(); })
.DistinctBy(e => e.ProviderId)
.ToArray();
} }
if (reader.TryGetString(index++, out var imageInfos)) if (reader.TryGetString(index++, out var imageInfos))

View File

@@ -452,6 +452,7 @@ namespace MediaBrowser.Controller.Entities
// That's all the new and changed ones - now see if any have been removed and need cleanup // That's all the new and changed ones - now see if any have been removed and need cleanup
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
var shouldRemove = !IsRoot || allowRemoveRoot; var shouldRemove = !IsRoot || allowRemoveRoot;
var actuallyRemoved = new List<BaseItem>();
// If it's an AggregateFolder, don't remove // If it's an AggregateFolder, don't remove
if (shouldRemove && itemsRemoved.Count > 0) if (shouldRemove && itemsRemoved.Count > 0)
{ {
@@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities
{ {
Logger.LogDebug("Removed item: {Path}", item.Path); Logger.LogDebug("Removed item: {Path}", item.Path);
actuallyRemoved.Add(item);
item.SetParent(null); item.SetParent(null);
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
} }
@@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities
{ {
LibraryManager.CreateItems(newItems, this, cancellationToken); LibraryManager.CreateItems(newItems, this, cancellationToken);
} }
// After removing items, reattach any detached user data to remaining children
// that share the same user data keys (eg. same episode replaced with a new file).
if (actuallyRemoved.Count > 0)
{
var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
foreach (var child in validChildren)
{
if (child.GetUserDataKeys().Any(removedKeys.Contains))
{
await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
}
}
}
} }
else else
{ {

View File

@@ -451,7 +451,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual) if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual)
{ {
return true; return episodeItem.Season is null or { LocationType: LocationType.Virtual };
} }
var season = episodeItem.Season; var season = episodeItem.Season;

View File

@@ -61,9 +61,10 @@ namespace MediaBrowser.Controller.Playlists
/// </summary> /// </summary>
/// <param name="playlistId">The playlist identifier.</param> /// <param name="playlistId">The playlist identifier.</param>
/// <param name="itemIds">The item ids.</param> /// <param name="itemIds">The item ids.</param>
/// <param name="position">Optional. 0-based index where to place the items or at the end if null.</param>
/// <param name="userId">The user identifier.</param> /// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId); Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId);
/// <summary> /// <summary>
/// Removes from playlist. /// Removes from playlist.

View File

@@ -83,6 +83,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"Smith/Kotzen", "Smith/Kotzen",
"We;Na", "We;Na",
"LSR/CITY", "LSR/CITY",
"Kairon; IRSE!",
}; };
/// <summary> /// <summary>

View File

@@ -1,4 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@@ -19,7 +18,7 @@ namespace MediaBrowser.Model.Providers
/// Gets or sets the name. /// Gets or sets the name.
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string Name { get; set; } public string? Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the provider ids. /// Gets or sets the provider ids.
@@ -41,13 +40,13 @@ namespace MediaBrowser.Model.Providers
public DateTime? PremiereDate { get; set; } public DateTime? PremiereDate { get; set; }
public string ImageUrl { get; set; } public string? ImageUrl { get; set; }
public string SearchProviderName { get; set; } public string? SearchProviderName { get; set; }
public string Overview { get; set; } public string? Overview { get; set; }
public RemoteSearchResult AlbumArtist { get; set; } public RemoteSearchResult? AlbumArtist { get; set; }
public RemoteSearchResult[] Artists { get; set; } public RemoteSearchResult[] Artists { get; set; }
} }

View File

@@ -33,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api
/// <returns>The image portion of the TMDb client configuration.</returns> /// <returns>The image portion of the TMDb client configuration.</returns>
[HttpGet("ClientConfiguration")] [HttpGet("ClientConfiguration")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ConfigImageTypes> TmdbClientConfiguration() public async Task<ConfigImageTypes?> TmdbClientConfiguration()
{ {
return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images; return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images;
} }

View File

@@ -75,10 +75,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var posters = collection.Images.Posters; var posters = collection.Images.Posters;
var backdrops = collection.Images.Backdrops; var backdrops = collection.Images.Backdrops;
var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count); var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0);
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); if (posters is not null)
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); {
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
return remoteImages; return remoteImages;
} }

View File

@@ -67,10 +67,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
return new[] { result }; return [result];
} }
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (collectionSearchResults is null)
{
return [];
}
var collections = new RemoteSearchResult[collectionSearchResults.Count]; var collections = new RemoteSearchResult[collectionSearchResults.Count];
for (var i = 0; i < collectionSearchResults.Count; i++) for (var i = 0; i < collectionSearchResults.Count; i++)

View File

@@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieTmdbId <= 0) if (movieTmdbId <= 0)
{ {
return Enumerable.Empty<RemoteImageInfo>(); return [];
} }
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
@@ -89,17 +89,28 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movie?.Images is null) if (movie?.Images is null)
{ {
return Enumerable.Empty<RemoteImageInfo>(); return [];
} }
var posters = movie.Images.Posters; var posters = movie.Images.Posters;
var backdrops = movie.Images.Backdrops; var backdrops = movie.Images.Backdrops;
var logos = movie.Images.Logos; var logos = movie.Images.Logos;
var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count); var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0);
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); if (posters is not null)
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); {
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
if (logos is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
return remoteImages; return remoteImages;
} }

View File

@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find; using TMDbLib.Objects.Find;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search; using TMDbLib.Objects.Search;
namespace MediaBrowser.Providers.Plugins.Tmdb.Movies namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
@@ -84,7 +85,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId); remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId);
return new[] { remoteResult }; return [remoteResult];
} }
} }
@@ -118,6 +119,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
.ConfigureAwait(false); .ConfigureAwait(false);
} }
if (movieResults is null)
{
return [];
}
var len = movieResults.Count; var len = movieResults.Count;
var remoteSearchResults = new RemoteSearchResult[len]; var remoteSearchResults = new RemoteSearchResult[len];
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++)
@@ -158,7 +164,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (searchResults.Count > 0) if (searchResults?.Count > 0)
{ {
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -167,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId)) if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
{ {
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (movieResultFromImdbId?.MovieResults.Count > 0) if (movieResultFromImdbId?.MovieResults?.Count > 0)
{ {
tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -193,7 +199,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
OriginalTitle = movieResult.OriginalTitle, OriginalTitle = movieResult.OriginalTitle,
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture), Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
Tagline = movieResult.Tagline, Tagline = movieResult.Tagline,
ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray() ProductionLocations = movieResult.ProductionCountries?.Select(pc => pc.Name).ToArray() ?? Array.Empty<string>()
}; };
var metadataResult = new MetadataResult<Movie> var metadataResult = new MetadataResult<Movie>
{ {
@@ -218,14 +224,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)); var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase));
if (ourRelease is not null) if (ourRelease?.Certification is not null)
{ {
movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification); movie.OfficialRating = TmdbUtils.BuildParentalRating(info.MetadataCountryCode, ourRelease.Certification);
} }
else else
{ {
var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
if (usRelease is not null) if (usRelease?.Certification is not null)
{ {
movie.OfficialRating = usRelease.Certification; movie.OfficialRating = usRelease.Certification;
} }
@@ -242,16 +248,23 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var genres = movieResult.Genres; var genres = movieResult.Genres;
foreach (var genre in genres.Select(g => g.Name).Trimmed()) if (genres is not null)
{ {
movie.AddGenre(genre); foreach (var genre in genres.Select(g => g.Name).Trimmed())
{
movie.AddGenre(genre);
}
} }
if (movieResult.Keywords?.Keywords is not null) if (movieResult.Keywords?.Keywords is not null)
{ {
for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++) foreach (var keyword in movieResult.Keywords.Keywords)
{ {
movie.AddTag(movieResult.Keywords.Keywords[i].Name); var name = keyword.Name;
if (!string.IsNullOrWhiteSpace(name))
{
movie.AddTag(name);
}
} }
} }

View File

@@ -56,13 +56,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
} }
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture)); result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds?.ImdbId);
return new[] { result }; return [result];
} }
} }
var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
if (personSearchResult is null)
{
return [];
}
var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count]; var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count];
for (var i = 0; i < personSearchResult.Count; i++) for (var i = 0; i < personSearchResult.Count; i++)
@@ -91,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId <= 0) if (personTmdbId <= 0)
{ {
var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false); var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false);
if (personSearchResults.Count > 0) if (personSearchResults?.Count > 0)
{ {
personTmdbId = personSearchResults[0].Id; personTmdbId = personSearchResults[0].Id;
} }

View File

@@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item.Name = seasonResult.Name; result.Item.Name = seasonResult.Name;
} }
result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId); result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds?.TvdbId);
// TODO why was this disabled? // TODO why was this disabled?
var credits = seasonResult.Credits; var credits = seasonResult.Credits;

View File

@@ -79,11 +79,22 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters; var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops; var backdrops = series.Images.Backdrops;
var logos = series.Images.Logos; var logos = series.Images.Logos;
var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count); var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0);
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); if (posters is not null)
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); {
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
if (logos is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
return remoteImages; return remoteImages;
} }

View File

@@ -112,6 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken) var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (tvSearchResults is null)
{
return [];
}
var remoteResults = new RemoteSearchResult[tvSearchResults.Count]; var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
for (var i = 0; i < tvSearchResults.Count; i++) for (var i = 0; i < tvSearchResults.Count; i++)
@@ -141,6 +145,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
} }
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult; return remoteResult;
} }
@@ -157,6 +162,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult; return remoteResult;
} }
@@ -174,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
{ {
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (searchResult?.TvResults.Count > 0) if (searchResult?.TvResults?.Count > 0)
{ {
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -183,7 +189,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)) if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
{ {
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (searchResult?.TvResults.Count > 0) if (searchResult?.TvResults?.Count > 0)
{ {
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -198,7 +204,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var cleanedName = TmdbUtils.CleanName(parsedName.Name); var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false); var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
if (searchResults.Count > 0) if (searchResults?.Count > 0)
{ {
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -262,15 +268,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (seriesResult.Keywords?.Results is not null) if (seriesResult.Keywords?.Results is not null)
{ {
for (var i = 0; i < seriesResult.Keywords.Results.Count; i++) foreach (var result in seriesResult.Keywords.Results)
{ {
series.AddTag(seriesResult.Keywords.Results[i].Name); var name = result.Name;
if (!string.IsNullOrWhiteSpace(name))
{
series.AddTag(name);
}
} }
} }
series.HomePageUrl = seriesResult.Homepage; series.HomePageUrl = seriesResult.Homepage;
series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); series.RunTimeTicks = seriesResult.EpisodeRunTime?.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus)) if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus))
{ {
@@ -279,6 +289,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.EndDate = seriesResult.LastAirDate; series.EndDate = seriesResult.LastAirDate;
series.PremiereDate = seriesResult.FirstAirDate; series.PremiereDate = seriesResult.FirstAirDate;
series.ProductionYear = seriesResult.FirstAirDate?.Year;
var ids = seriesResult.ExternalIds; var ids = seriesResult.ExternalIds;
if (ids is not null) if (ids is not null)
@@ -288,21 +299,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId); series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
} }
var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>(); var contentRatings = seriesResult.ContentRatings?.Results ?? new List<ContentRating>();
var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
var minimumRelease = contentRatings.FirstOrDefault(); var minimumRelease = contentRatings.FirstOrDefault();
if (ourRelease is not null) if (ourRelease?.Rating is not null)
{ {
series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating); series.OfficialRating = TmdbUtils.BuildParentalRating(preferredCountryCode, ourRelease.Rating);
} }
else if (usRelease is not null) else if (usRelease?.Rating is not null)
{ {
series.OfficialRating = usRelease.Rating; series.OfficialRating = usRelease.Rating;
} }
else if (minimumRelease is not null) else if (minimumRelease?.Rating is not null)
{ {
series.OfficialRating = minimumRelease.Rating; series.OfficialRating = minimumRelease.Rating;
} }
@@ -347,7 +358,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Role = actor.Character?.Trim() ?? string.Empty, Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor, Type = PersonKind.Actor,
SortOrder = actor.Order, SortOrder = actor.Order,
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath) // NOTE: Null values are filtered out above
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!)
}; };
if (actor.Id > 0) if (actor.Id > 0)
@@ -388,7 +400,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Name = crewMember.Name.Trim(), Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty, Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType, Type = entry.PersonType,
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath) // NOTE: Null values are filtered out above
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath!)
}; };
if (crewMember.Id > 0) if (crewMember.Id > 0)

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
@@ -195,7 +194,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false); await EnsureClientConfigAsync().ConfigureAwait(false);
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false); var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id; var episodeGroupId = series?.EpisodeGroups?.Results?.Find(g => g.Type == groupType)?.Id;
if (episodeGroupId is null) if (episodeGroupId is null)
{ {
@@ -263,7 +262,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv episode information or null if not found.</returns> /// <returns>The TMDb tv episode information or null if not found.</returns>
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken) public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, long episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
{ {
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
if (_memoryCache.TryGetValue(key, out TvEpisode? episode)) if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
@@ -276,9 +275,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false); var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
if (group is not null) if (group is not null)
{ {
var season = group.Groups.Find(s => s.Order == seasonNumber); var season = group.Groups?.Find(s => s.Order == seasonNumber);
// Episode order starts at 0 // Episode order starts at 0
var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1); var ep = season?.Episodes?.Find(e => e.Order == episodeNumber - 1);
if (ep is not null) if (ep is not null)
{ {
seasonNumber = ep.SeasonNumber; seasonNumber = ep.SeasonNumber;
@@ -382,7 +381,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="year">The year the tv show first aired.</param> /// <param name="year">The year the tv show first aired.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information.</returns> /// <returns>The TMDb tv show information.</returns>
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<SearchTv>?> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default)
{ {
var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null)
@@ -396,12 +395,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken) .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults.Results.Count > 0) if (searchResults?.Results?.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults.Results; return searchResults?.Results;
} }
/// <summary> /// <summary>
@@ -410,7 +409,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="name">The name of the person.</param> /// <param name="name">The name of the person.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb person information.</returns> /// <returns>The TMDb person information.</returns>
public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken) public async Task<IReadOnlyList<SearchPerson>?> SearchPersonAsync(string name, CancellationToken cancellationToken)
{ {
var key = $"searchperson-{name}"; var key = $"searchperson-{name}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null)
@@ -424,12 +423,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken) .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults.Results.Count > 0) if (searchResults?.Results?.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults.Results; return searchResults?.Results;
} }
/// <summary> /// <summary>
@@ -439,7 +438,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="language">The movie's language.</param> /// <param name="language">The movie's language.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns> /// <returns>The TMDb movie information.</returns>
public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken) public Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
{ {
return SearchMovieAsync(name, 0, language, null, cancellationToken); return SearchMovieAsync(name, 0, language, null, cancellationToken);
} }
@@ -453,7 +452,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns> /// <returns>The TMDb movie information.</returns>
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken) public async Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken)
{ {
var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null)
@@ -467,12 +466,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken) .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults.Results.Count > 0) if (searchResults?.Results?.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults.Results; return searchResults?.Results;
} }
/// <summary> /// <summary>
@@ -483,7 +482,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb collection information.</returns> /// <returns>The TMDb collection information.</returns>
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken) public async Task<IReadOnlyList<SearchCollection>?> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken)
{ {
var key = $"collectionsearch-{name}-{language}"; var key = $"collectionsearch-{name}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null)
@@ -497,12 +496,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken) .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults.Results.Count > 0) if (searchResults?.Results?.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults.Results; return searchResults?.Results;
} }
/// <summary> /// <summary>
@@ -511,14 +510,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="size">The image size to fetch.</param> /// <param name="size">The image size to fetch.</param>
/// <param name="path">The relative URL of the image.</param> /// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns> /// <returns>The absolute URL.</returns>
private string? GetUrl(string? size, string path) private string? GetUrl(string? size, string? path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
return null; return null;
} }
return _tmDbClient.GetImageUrl(size, path, true).ToString(); // Use "original" as default size if size is null or empty to prevent malformed URLs
var imageSize = string.IsNullOrEmpty(size) ? "original" : size;
return _tmDbClient.GetImageUrl(imageSize, path, true).ToString();
} }
/// <summary> /// <summary>
@@ -526,7 +528,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary> /// </summary>
/// <param name="posterPath">The relative URL of the poster.</param> /// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns> /// <returns>The absolute URL.</returns>
public string? GetPosterUrl(string posterPath) public string? GetPosterUrl(string? posterPath)
{ {
return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath); return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
} }
@@ -536,7 +538,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary> /// </summary>
/// <param name="actorProfilePath">The relative URL of the profile image.</param> /// <param name="actorProfilePath">The relative URL of the profile image.</param>
/// <returns>The absolute URL.</returns> /// <returns>The absolute URL.</returns>
public string? GetProfileUrl(string actorProfilePath) public string? GetProfileUrl(string? actorProfilePath)
{ {
return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath); return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
} }
@@ -639,30 +641,44 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
private static void ValidatePreferences(TMDbConfig config) private static void ValidatePreferences(TMDbConfig config)
{ {
var imageConfig = config.Images; var imageConfig = config.Images;
if (imageConfig is null)
{
return;
}
var pluginConfig = Plugin.Instance.Configuration; var pluginConfig = Plugin.Instance.Configuration;
if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize)) if (imageConfig.PosterSizes is not null
&& pluginConfig.PosterSize is not null
&& !imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
{ {
pluginConfig.PosterSize = imageConfig.PosterSizes[^1]; pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
} }
if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize)) if (imageConfig.BackdropSizes is not null
&& pluginConfig.BackdropSize is not null
&& !imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
{ {
pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1]; pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
} }
if (!imageConfig.LogoSizes.Contains(pluginConfig.LogoSize)) if (imageConfig.LogoSizes is not null
&& pluginConfig.LogoSize is not null
&& !imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
{ {
pluginConfig.LogoSize = imageConfig.LogoSizes[^1]; pluginConfig.LogoSize = imageConfig.LogoSizes[^1];
} }
if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize)) if (imageConfig.ProfileSizes is not null
&& pluginConfig.ProfileSize is not null
&& !imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
{ {
pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1]; pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
} }
if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize)) if (imageConfig.StillSizes is not null
&& pluginConfig.StillSize is not null
&& !imageConfig.StillSizes.Contains(pluginConfig.StillSize))
{ {
pluginConfig.StillSize = imageConfig.StillSizes[^1]; pluginConfig.StillSize = imageConfig.StillSizes[^1];
} }

View File

@@ -69,20 +69,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The Jellyfin person type.</returns> /// <returns>The Jellyfin person type.</returns>
public static PersonKind MapCrewToPersonType(Crew crew) public static PersonKind MapCrewToPersonType(Crew crew)
{ {
if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase) if (string.Equals(crew.Department, "directing", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase)) && string.Equals(crew.Job, "director", StringComparison.OrdinalIgnoreCase))
{ {
return PersonKind.Director; return PersonKind.Director;
} }
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) if (string.Equals(crew.Department, "production", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase)) && string.Equals(crew.Job, "producer", StringComparison.OrdinalIgnoreCase))
{ {
return PersonKind.Producer; return PersonKind.Producer;
} }
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase) if (string.Equals(crew.Department, "writing", StringComparison.OrdinalIgnoreCase)
&& (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase))) && (string.Equals(crew.Job, "writer", StringComparison.OrdinalIgnoreCase) || string.Equals(crew.Job, "screenplay", StringComparison.OrdinalIgnoreCase)))
{ {
return PersonKind.Writer; return PersonKind.Writer;
} }
@@ -97,9 +97,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>A boolean indicating whether the video is a trailer.</returns> /// <returns>A boolean indicating whether the video is a trailer.</returns>
public static bool IsTrailerType(Video video) public static bool IsTrailerType(Video video)
{ {
return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase) return string.Equals(video.Site, "youtube", StringComparison.OrdinalIgnoreCase)
&& (video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase) && (string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase)
|| video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase)); || string.Equals(video.Type, "teaser", StringComparison.OrdinalIgnoreCase));
} }
/// <summary> /// <summary>
@@ -117,14 +117,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode); preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
languages.Add(preferredLanguage); languages.Add(preferredLanguage);
if (preferredLanguage.Length == 5) // Like en-US
{
// Currently, TMDb supports 2-letter language codes only.
// They are planning to change this in the future, thus we're
// supplying both codes if we're having a 5-letter code.
languages.Add(preferredLanguage.Substring(0, 2));
}
} }
languages.Add("null"); languages.Add("null");
@@ -185,10 +177,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguage">The image's actual language code.</param> /// <param name="imageLanguage">The image's actual language code.</param>
/// <param name="requestLanguage">The requested language code.</param> /// <param name="requestLanguage">The requested language code.</param>
/// <returns>The language code.</returns> /// <returns>The language code.</returns>
public static string AdjustImageLanguage(string imageLanguage, string requestLanguage) public static string AdjustImageLanguage(string? imageLanguage, string requestLanguage)
{ {
if (!string.IsNullOrEmpty(imageLanguage) if (string.IsNullOrEmpty(imageLanguage))
&& !string.IsNullOrEmpty(requestLanguage) {
return string.Empty;
}
if (!string.IsNullOrEmpty(requestLanguage)
&& requestLanguage.Length > 2 && requestLanguage.Length > 2
&& imageLanguage.Length == 2 && imageLanguage.Length == 2
&& requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase)) && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@@ -201,6 +202,26 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
false); false);
} }
private static bool NeedsVirtualSeason(Episode episode, HashSet<Guid> physicalSeasonIds, HashSet<string> physicalSeasonPaths)
{
// Episode has a known season number, needs a season
if (episode.ParentIndexNumber.HasValue)
{
return true;
}
// Not yet processed
if (episode.SeasonId.IsEmpty())
{
return false;
}
// Episode has been processed, only needs a virtual season if it isn't
// already linked to a known physical season by ID or path
return !physicalSeasonIds.Contains(episode.SeasonId)
&& !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty);
}
/// <summary> /// <summary>
/// Creates seasons for all episodes if they don't exist. /// Creates seasons for all episodes if they don't exist.
/// If no season number can be determined, a dummy season will be created. /// If no season number can be determined, a dummy season will be created.
@@ -212,8 +233,20 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{ {
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
var seasons = seriesChildren.OfType<Season>().ToList(); var seasons = seriesChildren.OfType<Season>().ToList();
var physicalSeasonIds = seasons
.Where(e => e.LocationType != LocationType.Virtual)
.Select(e => e.Id)
.ToHashSet();
var physicalSeasonPathSet = seasons
.Where(e => e.LocationType != LocationType.Virtual && !string.IsNullOrEmpty(e.Path))
.Select(e => e.Path)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var uniqueSeasonNumbers = seriesChildren var uniqueSeasonNumbers = seriesChildren
.OfType<Episode>() .OfType<Episode>()
.Where(e => NeedsVirtualSeason(e, physicalSeasonIds, physicalSeasonPathSet))
.Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null) .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
.Distinct(); .Distinct();

View File

@@ -94,13 +94,12 @@ git clone https://github.com/jellyfin/jellyfin.git
The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly. The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly.
Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step. Note that it is recommended for development to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step.
There are three options to get the files for the web client. There are two options to get the files for the web client.
1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page. 1. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) 2. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
### Running The Server ### Running The Server

View File

@@ -131,7 +131,7 @@ namespace Jellyfin.Extensions
/// </summary> /// </summary>
/// <param name="values">The enumerable of strings to trim.</param> /// <param name="values">The enumerable of strings to trim.</param>
/// <returns>The enumeration of trimmed strings.</returns> /// <returns>The enumeration of trimmed strings.</returns>
public static IEnumerable<string> Trimmed(this IEnumerable<string> values) public static IEnumerable<string> Trimmed(this IEnumerable<string?> values)
{ {
return values.Select(i => (i ?? string.Empty).Trim()); return values.Select(i => (i ?? string.Empty).Trim());
} }

View File

@@ -11,21 +11,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")] [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)] [InlineData("Superman: Red Son", "imdbid", null)]
[InlineData("Superman: Red Son", "something", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid1-tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")] [InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")] [InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")] [InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")] [InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")] [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")] [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")] [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
[InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")] [InlineData("[tmdbid=618355]", "tmdbid", "618355")]
[InlineData("{tmdbid=618355}", "tmdbid", "618355")]
[InlineData("(tmdbid=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")] [InlineData("[tmdbid-618355]", "tmdbid", "618355")]
[InlineData("{tmdbid-618355)", "tmdbid", null)]
[InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")] [InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")] [InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)] [InlineData("tmdbid=618355]", "tmdbid", null)]
@@ -36,6 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)] [InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)] [InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")] [InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
[InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
[InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
[InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult) public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{ {
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute)); Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));