aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.devcontainer/devcontainer.json24
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-compat.yml4
-rw-r--r--.github/workflows/ci-openapi.yml6
-rw-r--r--.github/workflows/commands.yml2
-rw-r--r--.github/workflows/issue-stale.yml2
-rw-r--r--.github/workflows/project-automation.yml2
-rw-r--r--.github/workflows/pull-request-conflict.yml4
-rw-r--r--.github/workflows/pull-request-stale.yaml2
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props42
-rw-r--r--Emby.Naming/TV/TvParserHelpers.cs2
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs28
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json20
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs42
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs16
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs2
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs1
-rw-r--r--MediaBrowser.Model/Providers/RemoteSearchResult.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs35
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs39
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs70
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs28
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs33
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs2
39 files changed, 357 insertions, 175 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 1d65527d9..302ac67b6 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "10.0.2",
+ "version": "10.0.3",
"commands": [
"dotnet-ef"
]
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 8b6b12c31..c67c29237 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,17 +1,31 @@
{
"name": "Development Jellyfin Server",
- "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
+ "image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// 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\"",
- // reads the extensions list and installs them
- "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
+ // The previous way of installing extensions via the vs command dont work on selfhosted devcontainers
+ "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": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
- "dotnetRuntimeVersions": "9.0",
- "aspNetCoreRuntimeVersions": "9.0"
+ "dotnetRuntimeVersions": "10.0",
+ "aspNetCoreRuntimeVersions": "10.0"
},
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"preserve_apt_list": false,
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 9072fa9f9..66fa73d25 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -28,13 +28,13 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
+ uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
+ uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
+ uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index 23a82a1b2..8e3717b33 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -1,6 +1,6 @@
name: ABI Compatibility
on:
- pull_request_target:
+ pull_request:
permissions: {}
@@ -77,7 +77,7 @@ jobs:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
name: ABI - Difference
- if: ${{ github.event_name == 'pull_request_target' }}
+ if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- abi-head
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 08eedd54f..3d04ac5e0 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -5,7 +5,7 @@ on:
- master
tags:
- 'v*'
- pull_request_target:
+ pull_request:
permissions: {}
@@ -78,7 +78,7 @@ jobs:
pull-requests: write
name: OpenAPI - Difference
- if: ${{ github.event_name == 'pull_request_target' }}
+ if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- openapi-head
@@ -109,7 +109,7 @@ jobs:
publish-unstable:
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
needs:
- openapi-head
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 2c4efcc8c..2adb8f101 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -4,7 +4,7 @@ on:
types:
- created
- edited
- pull_request_target:
+ pull_request:
types:
- labeled
- synchronize
diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml
index cb535297e..339fcf569 100644
--- a/.github/workflows/issue-stale.yml
+++ b/.github/workflows/issue-stale.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
+ - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml
index 7b29d3c81..9a9f3214a 100644
--- a/.github/workflows/project-automation.yml
+++ b/.github/workflows/project-automation.yml
@@ -4,7 +4,7 @@ on:
push:
branches:
- master
- pull_request_target:
+ pull_request:
issue_comment:
permissions: {}
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
index e6a9bf0ca..b003636a6 100644
--- a/.github/workflows/pull-request-conflict.yml
+++ b/.github/workflows/pull-request-conflict.yml
@@ -4,7 +4,7 @@ on:
push:
branches:
- master
- pull_request_target:
+ pull_request:
issue_comment:
permissions: {}
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Apply label
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:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml
index 0d74e643e..e114276c2 100644
--- a/.github/workflows/pull-request-stale.yaml
+++ b/.github/workflows/pull-request-stale.yaml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
+ - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 1770db60b..cb7d3fbbc 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -287,3 +287,4 @@
- [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
+ - [DerMaddis](https://github.com/dermaddis)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 668b60109..a520b87e2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
- <PackageVersion Include="AsyncKeyedLock" Version="8.0.1" />
+ <PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" 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" Version="1.4.0-pre.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="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
@@ -26,27 +26,27 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" 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.Data.Sqlite" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
@@ -77,10 +77,10 @@
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
<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="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="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
diff --git a/Emby.Naming/TV/TvParserHelpers.cs b/Emby.Naming/TV/TvParserHelpers.cs
index 029917858..706251f29 100644
--- a/Emby.Naming/TV/TvParserHelpers.cs
+++ b/Emby.Naming/TV/TvParserHelpers.cs
@@ -18,7 +18,7 @@ public static class TvParserHelpers
/// <param name="status">The status string.</param>
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
/// <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))
{
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 4874eca8e..996cd1b3c 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -267,22 +267,24 @@ namespace Emby.Server.Implementations.Images
{
var image = item.GetImageInfo(type, 0);
- if (image is not null)
+ if (image is null)
{
- if (!image.IsLocalFile)
- {
- return false;
- }
+ return GetItemsWithImages(item).Count is not 0;
+ }
- if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
- {
- return false;
- }
+ if (!image.IsLocalFile)
+ {
+ return false;
+ }
- if (!HasChangedByDate(item, image))
- {
- return false;
- }
+ if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
+ {
+ return false;
+ }
+
+ if (!HasChangedByDate(item, image))
+ {
+ return false;
}
return true;
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 3d598c491..cb11cc089 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -3,7 +3,7 @@
"Playlists": "Плэй-лісты",
"Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}",
- "ItemAddedWithName": "{0} даданы ў бібліятэку",
+ "ItemAddedWithName": "{0} дададзены ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны",
@@ -14,7 +14,7 @@
"Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі",
- "Default": "Па змаўчанні",
+ "Default": "Прадвызначана",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі",
"Favorites": "Абранае",
@@ -81,8 +81,8 @@
"NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
- "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
- "NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
+ "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена",
+ "NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены",
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 82cc1857b..1e7279be8 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres",
"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.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index ff60e6127..f0c4b5027 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -61,7 +61,7 @@
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto",
- "Playlists": "Playlist",
+ "Playlists": "Scalette",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} è stato installato",
"PluginUninstalledWithName": "{0} è stato disinstallato",
@@ -114,20 +114,20 @@
"TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate",
- "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell’età configurata.",
+ "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell'età configurata.",
"Undefined": "Non specificato",
"Forced": "Forzato",
"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.",
"TaskOptimizeDatabase": "Ottimizza database",
"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",
"HearingImpaired": "Non udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
- "TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
- "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
+ "TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.",
"TaskAudioNormalization": "Normalizzazione dell'audio",
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index d564d54ce..bdca8ae1c 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -43,32 +43,32 @@
"NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}",
"NameSeasonUnknown": "シーズン不明",
- "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
+ "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロードできます。",
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
"NotificationOptionAudioPlayback": "オーディオの再生を開始",
- "NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました",
+ "NotificationOptionAudioPlaybackStopped": "オーディオの再生を停止",
"NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました",
"NotificationOptionInstallationFailed": "インストール失敗",
"NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました",
"NotificationOptionPluginError": "プラグインに障害が発生しました",
- "NotificationOptionPluginInstalled": "プラグインがインストールされました",
- "NotificationOptionPluginUninstalled": "プラグインがアンインストールされました",
+ "NotificationOptionPluginInstalled": "プラグインをインストールしました",
+ "NotificationOptionPluginUninstalled": "プラグインをアンインストールしました",
"NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました",
"NotificationOptionServerRestartRequired": "サーバーを再起動してください",
"NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗",
"NotificationOptionUserLockedOut": "ユーザーはロックされています",
- "NotificationOptionVideoPlayback": "ビデオの再生を開始しました",
- "NotificationOptionVideoPlaybackStopped": "ビデオを停止しました",
+ "NotificationOptionVideoPlayback": "ビデオの再生を開始",
+ "NotificationOptionVideoPlaybackStopped": "ビデオの再生を停止",
"Photos": "フォト",
"Playlists": "プレイリスト",
"Plugin": "プラグイン",
- "PluginInstalledWithName": "{0} がインストールされました",
- "PluginUninstalledWithName": "{0} がアンインストールされました",
- "PluginUpdatedWithName": "{0} が更新されました",
+ "PluginInstalledWithName": "{0} をインストールしました",
+ "PluginUninstalledWithName": "{0} をアンインストールしました",
+ "PluginUpdatedWithName": "{0} を更新しました",
"ProviderValue": "プロバイダ: {0}",
"ScheduledTaskFailedWithName": "{0} が失敗しました",
- "ScheduledTaskStartedWithName": "{0} が開始されました",
+ "ScheduledTaskStartedWithName": "{0} を開始",
"ServerNameNeedsToBeRestarted": "{0} を再起動してください",
"Shows": "番組",
"Songs": "曲",
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 1577c5c9c..409414139 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -198,17 +198,22 @@ namespace Emby.Server.Implementations.Playlists
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);
- return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
- {
- EnableImages = true
- });
+ return AddToPlaylistInternal(
+ playlistId,
+ 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
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
@@ -243,7 +248,30 @@ namespace Emby.Server.Implementations.Playlists
}
// 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;
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 59e6fd779..967918093 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -359,6 +359,7 @@ public class PlaylistsController : BaseJellyfinApiController
/// </summary>
/// <param name="playlistId">The playlist id.</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>
/// <response code="204">Items added to playlist.</response>
/// <response code="403">Access forbidden.</response>
@@ -371,6 +372,7 @@ public class PlaylistsController : BaseJellyfinApiController
public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
+ [FromQuery] int? position,
[FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -388,7 +390,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
+ await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, position, userId.Value).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 4b1e53a35..70761fa7d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -1163,7 +1163,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Item = null!,
ProviderId = e[0],
ProviderValue = string.Join('|', e.Skip(1))
- }).ToArray();
+ })
+ .DistinctBy(e => e.ProviderId)
+ .ToArray();
}
if (reader.TryGetString(index++, out var imageInfos))
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index d2a3290c4..2ecb6cbdf 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -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
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
var shouldRemove = !IsRoot || allowRemoveRoot;
+ var actuallyRemoved = new List<BaseItem>();
// If it's an AggregateFolder, don't remove
if (shouldRemove && itemsRemoved.Count > 0)
{
@@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities
{
Logger.LogDebug("Removed item: {Path}", item.Path);
+ actuallyRemoved.Add(item);
item.SetParent(null);
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
}
@@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities
{
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
{
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 6396631f9..6a26ecaeb 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -451,7 +451,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual)
{
- return true;
+ return episodeItem.Season is null or { LocationType: LocationType.Virtual };
}
var season = episodeItem.Season;
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index 497c4a511..92aa92396 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -61,9 +61,10 @@ namespace MediaBrowser.Controller.Playlists
/// </summary>
/// <param name="playlistId">The playlist identifier.</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>
/// <returns>Task.</returns>
- Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
+ Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId);
/// <summary>
/// Removes from playlist.
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 50f7716d8..dbe532289 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -83,6 +83,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"Smith/Kotzen",
"We;Na",
"LSR/CITY",
+ "Kairon; IRSE!",
};
/// <summary>
diff --git a/MediaBrowser.Model/Providers/RemoteSearchResult.cs b/MediaBrowser.Model/Providers/RemoteSearchResult.cs
index a29e7ad1c..7d3b5e4ab 100644
--- a/MediaBrowser.Model/Providers/RemoteSearchResult.cs
+++ b/MediaBrowser.Model/Providers/RemoteSearchResult.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -19,7 +18,7 @@ namespace MediaBrowser.Model.Providers
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the provider ids.
@@ -41,13 +40,13 @@ namespace MediaBrowser.Model.Providers
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; }
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
index 450ee2a33..3eacc4f0f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api
/// <returns>The image portion of the TMDb client configuration.</returns>
[HttpGet("ClientConfiguration")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ConfigImageTypes> TmdbClientConfiguration()
+ public async Task<ConfigImageTypes?> TmdbClientConfiguration()
{
return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index 02818a0e2..78be5804e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -75,10 +75,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var posters = collection.Images.Posters;
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));
- remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ if (posters is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
+ }
+
+ if (backdrops is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ }
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index 34c9abae1..a7bba2d53 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -67,10 +67,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
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);
+ if (collectionSearchResults is null)
+ {
+ return [];
+ }
var collections = new RemoteSearchResult[collectionSearchResults.Count];
for (var i = 0; i < collectionSearchResults.Count; i++)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index fcc357410..714c57d36 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
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
@@ -89,17 +89,28 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movie?.Images is null)
{
- return Enumerable.Empty<RemoteImageInfo>();
+ return [];
}
var posters = movie.Images.Posters;
var backdrops = movie.Images.Backdrops;
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));
- remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
- remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
+ if (posters is not null)
+ {
+ 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;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 2beb34e43..ff584ba1d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find;
+using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
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.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId);
- return new[] { remoteResult };
+ return [remoteResult];
}
}
@@ -118,6 +119,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
.ConfigureAwait(false);
}
+ if (movieResults is null)
+ {
+ return [];
+ }
+
var len = movieResults.Count;
var remoteSearchResults = new RemoteSearchResult[len];
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);
- if (searchResults.Count > 0)
+ if (searchResults?.Count > 0)
{
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
@@ -167,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
{
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);
}
@@ -193,7 +199,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
OriginalTitle = movieResult.OriginalTitle,
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
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>
{
@@ -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));
- 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
{
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;
}
@@ -242,16 +248,23 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
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)
{
- 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);
+ }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 4b32d0f6b..64ab98b26 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -56,13 +56,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
}
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);
+ if (personSearchResult is null)
+ {
+ return [];
+ }
var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count];
for (var i = 0; i < personSearchResult.Count; i++)
@@ -91,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId <= 0)
{
var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false);
- if (personSearchResults.Count > 0)
+ if (personSearchResults?.Count > 0)
{
personTmdbId = personSearchResults[0].Id;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 0905a3bdc..1eb522137 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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?
var credits = seasonResult.Credits;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 5cba84dcb..f2e7d0c6e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -79,11 +79,22 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops;
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));
- remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
- remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
+ if (posters is not null)
+ {
+ 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;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 82d4e5838..7e36c1e20 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -112,6 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken)
.ConfigureAwait(false);
+ if (tvSearchResults is null)
+ {
+ return [];
+ }
var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
for (var i = 0; i < tvSearchResults.Count; i++)
@@ -141,6 +145,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+ remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult;
}
@@ -157,6 +162,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+ remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult;
}
@@ -174,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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);
- if (searchResult?.TvResults.Count > 0)
+ if (searchResult?.TvResults?.Count > 0)
{
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))
{
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);
}
@@ -198,7 +204,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
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);
}
@@ -262,15 +268,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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.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))
{
@@ -279,6 +289,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.EndDate = seriesResult.LastAirDate;
series.PremiereDate = seriesResult.FirstAirDate;
+ series.ProductionYear = seriesResult.FirstAirDate?.Year;
var ids = seriesResult.ExternalIds;
if (ids is not null)
@@ -288,21 +299,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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 usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
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;
}
- else if (minimumRelease is not null)
+ else if (minimumRelease?.Rating is not null)
{
series.OfficialRating = minimumRelease.Rating;
}
@@ -347,7 +358,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor,
SortOrder = actor.Order,
- ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
+ // NOTE: Null values are filtered out above
+ ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!)
};
if (actor.Id > 0)
@@ -388,7 +400,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType,
- ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
+ // NOTE: Null values are filtered out above
+ ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath!)
};
if (crewMember.Id > 0)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index fedf34598..274db347b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -195,7 +194,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().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)
{
@@ -263,7 +262,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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);
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
- 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)
{
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="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -410,7 +409,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="name">The name of the person.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -439,7 +438,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="language">The movie's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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);
}
@@ -453,7 +452,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -483,7 +482,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -511,14 +510,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="size">The image size to fetch.</param>
/// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns>
- private string? GetUrl(string? size, string path)
+ private string? GetUrl(string? size, string? path)
{
if (string.IsNullOrEmpty(path))
{
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>
@@ -526,7 +528,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns>
- public string? GetPosterUrl(string posterPath)
+ public string? GetPosterUrl(string? posterPath)
{
return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
}
@@ -536,7 +538,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="actorProfilePath">The relative URL of the profile image.</param>
/// <returns>The absolute URL.</returns>
- public string? GetProfileUrl(string actorProfilePath)
+ public string? GetProfileUrl(string? actorProfilePath)
{
return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
}
@@ -639,30 +641,44 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
private static void ValidatePreferences(TMDbConfig config)
{
var imageConfig = config.Images;
+ if (imageConfig is null)
+ {
+ return;
+ }
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];
}
- 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];
}
- 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];
}
- 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];
}
- 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];
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index 0944b557e..39c0497be 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -69,20 +69,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The Jellyfin person type.</returns>
public static PersonKind MapCrewToPersonType(Crew crew)
{
- if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase)
- && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(crew.Department, "directing", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(crew.Job, "director", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Director;
}
- if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
- && crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(crew.Department, "production", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(crew.Job, "producer", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Producer;
}
- if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
- && (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase)))
+ if (string.Equals(crew.Department, "writing", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(crew.Job, "writer", StringComparison.OrdinalIgnoreCase) || string.Equals(crew.Job, "screenplay", StringComparison.OrdinalIgnoreCase)))
{
return PersonKind.Writer;
}
@@ -97,9 +97,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>A boolean indicating whether the video is a trailer.</returns>
public static bool IsTrailerType(Video video)
{
- return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)
- && (video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
- || video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
+ return string.Equals(video.Site, "youtube", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(video.Type, "teaser", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
@@ -177,10 +177,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguage">The image's actual language code.</param>
/// <param name="requestLanguage">The requested language code.</param>
/// <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)
- && !string.IsNullOrEmpty(requestLanguage)
+ if (string.IsNullOrEmpty(imageLanguage))
+ {
+ return string.Empty;
+ }
+
+ if (!string.IsNullOrEmpty(requestLanguage)
&& requestLanguage.Length > 2
&& imageLanguage.Length == 2
&& requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index c3a6ddd6a..61a31fbfd 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -201,6 +202,26 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
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>
/// Creates seasons for all episodes if they don't exist.
/// 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 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
.OfType<Episode>()
+ .Where(e => NeedsVirtualSeason(e, physicalSeasonIds, physicalSeasonPathSet))
.Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
.Distinct();
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 60df47113..c7e8319f5 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -131,7 +131,7 @@ namespace Jellyfin.Extensions
/// </summary>
/// <param name="values">The enumerable of strings to trim.</param>
/// <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());
}