aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJPVenson <github@jpb.email>2025-03-25 15:12:48 +0000
committerJPVenson <github@jpb.email>2025-03-25 15:12:48 +0000
commit850f1c79f1319de56a300c1d62565c9b2793b7d8 (patch)
treef307a380c63d80809fed23c7ce2c8a0d0aba5de8
parentef7f6fc8a97118df7f410a7afa2f501f3f4ca3e2 (diff)
parent671d801d9f734665d0acbd441246712ad2e3d91f (diff)
Merge branch 'master' into feature/DatabaseRefactor
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml8
-rw-r--r--.github/workflows/ci-compat.yml12
-rw-r--r--.github/workflows/ci-openapi.yml20
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--.github/workflows/commands.yml2
-rw-r--r--.github/workflows/issue-template-check.yml2
-rw-r--r--Directory.Packages.props54
-rw-r--r--Emby.Naming/TV/SeasonPathParser.cs90
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs58
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs2
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs106
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs78
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs10
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs86
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs36
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs7
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json20
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs6
-rw-r--r--Emby.Server.Implementations/Localization/countries.json2
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs16
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs61
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs3
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs4
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs72
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs14
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs22
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs24
-rw-r--r--Jellyfin.Server/Program.cs35
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs172
-rw-r--r--MediaBrowser.Common/Configuration/IApplicationPaths.cs6
-rw-r--r--MediaBrowser.Common/Net/NetworkUtils.cs19
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs6
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs5
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs2
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs17
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs12
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs8
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs6
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs27
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs33
-rw-r--r--MediaBrowser.Model/Providers/ExternalIdInfo.cs14
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs113
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs29
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs32
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs52
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs7
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs32
-rw-r--r--MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs4
-rw-r--r--MediaBrowser.Providers/Music/ImvdbId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs32
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs32
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs95
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalId.cs3
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs24
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs18
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs97
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs7
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs4
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs12
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs13
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs42
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs68
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs52
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs5
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs2
105 files changed, 1454 insertions, 778 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index ea2675a3d..bc2098a53 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "9.0.2",
+ "version": "9.0.3",
"commands": [
"dotnet-ef"
]
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 1f166d10c..6a3d4d351 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -22,16 +22,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup .NET
- uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
+ uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
+ uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
+ uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index ca505790c..13b029e52 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -17,7 +17,7 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: '9.0.x'
@@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: abi-head
retention-days: 14
@@ -47,7 +47,7 @@ jobs:
fetch-depth: 0
- name: Setup .NET
- uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: '9.0.x'
@@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: abi-base
retention-days: 14
@@ -85,13 +85,13 @@ jobs:
steps:
- name: Download abi-head
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: abi-head
path: abi-head
- name: Download abi-base
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: abi-base
path: abi-base
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 85a7a33bc..95e090f9b 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -21,13 +21,13 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: '9.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: openapi-head
retention-days: 14
@@ -55,13 +55,13 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: '9.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: openapi-base
retention-days: 14
@@ -80,12 +80,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: openapi-base
path: openapi-base
@@ -158,7 +158,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: openapi-head
path: openapi-head
@@ -172,7 +172,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
- uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
+ uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -220,7 +220,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: openapi-head
path: openapi-head
@@ -234,7 +234,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
- uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
+ uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index ec78396db..04c8465e9 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: ${{ env.SDK_VERSION }}
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 1ab7ae029..082084ed4 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -46,7 +46,7 @@ jobs:
- name: install python
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
- name: install python packages
run: pip install -r rename/requirements.txt
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index 3c5ba68f9..e3e801956 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -16,7 +16,7 @@ jobs:
- name: install python
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
- name: install python packages
run: pip install -r main-repo-triage/requirements.txt
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 854c5a6df..89311142c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,31 +22,31 @@
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.10" />
- <PackageVersion Include="LrcParser" Version="2024.0728.2" />
+ <PackageVersion Include="LrcParser" Version="2025.228.1" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -75,11 +75,11 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
- <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
- <PackageVersion Include="System.Text.Json" Version="9.0.2" />
- <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
+ <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.3" />
+ <PackageVersion Include="System.Text.Json" Version="9.0.3" />
+ <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.3" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.17.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.19.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index 45b91971b..98ee1e4b8 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -1,43 +1,35 @@
using System;
using System.Globalization;
using System.IO;
+using System.Text.RegularExpressions;
namespace Emby.Naming.TV
{
/// <summary>
/// Class to parse season paths.
/// </summary>
- public static class SeasonPathParser
+ public static partial class SeasonPathParser
{
- /// <summary>
- /// A season folder must contain one of these somewhere in the name.
- /// </summary>
- private static readonly string[] _seasonFolderNames =
- {
- "season",
- "sæson",
- "temporada",
- "saison",
- "staffel",
- "series",
- "сезон",
- "stagione"
- };
-
- private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
+ [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$")]
+ private static partial Regex ProcessPre();
+
+ [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")]
+ private static partial Regex ProcessPost();
/// <summary>
/// Attempts to parse season number from path.
/// </summary>
/// <param name="path">Path to season.</param>
+ /// <param name="parentPath">Folder name of the parent.</param>
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
- public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
+ public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
+ var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
- var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
+ var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
result.SeasonNumber = seasonNumber;
@@ -54,15 +46,24 @@ namespace Emby.Naming.TV
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="parentFolderName">The parent folder name.</param>
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path,
+ string? parentFolderName,
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
{
string filename = Path.GetFileName(path);
+ filename = Regex.Replace(filename, "[ ._-]", string.Empty);
+
+ if (parentFolderName is not null)
+ {
+ parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
+ filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
if (supportSpecialAliases)
{
@@ -85,53 +86,38 @@ namespace Emby.Naming.TV
}
}
- if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
+ if (filename.StartsWith('s'))
{
- return (seasonNumber, true);
- }
+ var testFilename = filename.AsSpan()[1..];
- // Look for one of the season folder names
- foreach (var name in _seasonFolderNames)
- {
- if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
+ if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
- var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
- if (result.SeasonNumber.HasValue)
- {
- return result;
- }
-
- break;
+ return (val, true);
}
}
- var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
- foreach (var part in parts)
+ var preMatch = ProcessPre().Match(filename);
+ if (preMatch.Success)
{
- if (TryGetSeasonNumberFromPart(part, out seasonNumber))
- {
- return (seasonNumber, true);
- }
+ return CheckMatch(preMatch);
}
-
- return (null, true);
- }
-
- private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
- {
- seasonNumber = 0;
- if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
+ else
{
- return false;
+ var postMatch = ProcessPost().Match(filename);
+ return CheckMatch(postMatch);
}
+ }
- if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
+ {
+ var numberString = match.Groups["seasonnumber"];
+ if (numberString.Success)
{
- seasonNumber = value;
- return true;
+ var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
+ return (seasonNumber, true);
}
- return false;
+ return (null, false);
}
/// <summary>
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index dc845b2d7..f0cca9efd 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
- /// <summary>
- /// Gets the path to the program data folder.
- /// </summary>
- /// <value>The program data path.</value>
+ /// <inheritdoc/>
public string ProgramDataPath { get; }
/// <inheritdoc/>
public string WebPath { get; }
- /// <summary>
- /// Gets the path to the system folder.
- /// </summary>
- /// <value>The path to the system folder.</value>
+ /// <inheritdoc/>
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
- /// <summary>
- /// Gets the folder path to the data directory.
- /// </summary>
- /// <value>The data directory.</value>
+ /// <inheritdoc/>
public string DataPath { get; }
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
- /// <summary>
- /// Gets the image cache path.
- /// </summary>
- /// <value>The image cache path.</value>
+ /// <inheritdoc/>
public string ImageCachePath => Path.Combine(CachePath, "images");
- /// <summary>
- /// Gets the path to the plugin directory.
- /// </summary>
- /// <value>The plugins path.</value>
+ /// <inheritdoc/>
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
- /// <summary>
- /// Gets the path to the plugin configurations directory.
- /// </summary>
- /// <value>The plugin configurations path.</value>
+ /// <inheritdoc/>
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
- /// <summary>
- /// Gets the path to the log directory.
- /// </summary>
- /// <value>The log directory path.</value>
+ /// <inheritdoc/>
public string LogDirectoryPath { get; }
- /// <summary>
- /// Gets the path to the application configuration root directory.
- /// </summary>
- /// <value>The configuration directory path.</value>
+ /// <inheritdoc/>
public string ConfigurationDirectoryPath { get; }
- /// <summary>
- /// Gets the path to the system configuration file.
- /// </summary>
- /// <value>The system configuration file path.</value>
+ /// <inheritdoc/>
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
- /// <summary>
- /// Gets or sets the folder path to the cache directory.
- /// </summary>
- /// <value>The cache directory.</value>
+ /// <inheritdoc/>
public string CachePath { get; set; }
- /// <summary>
- /// Gets the folder path to the temp directory within the cache folder.
- /// </summary>
- /// <value>The temp directory.</value>
+ /// <inheritdoc/>
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
+
+ /// <inheritdoc />
+ public string TrickplayPath => Path.Combine(DataPath, "trickplay");
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index eb01ed0fe..6e8a9fc20 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -57,6 +57,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
@@ -508,6 +509,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
+ serviceCollection.AddSingleton<IPathManager, PathManager>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 7ea863d76..a83ded439 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -5,80 +5,80 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Data
+namespace Emby.Server.Implementations.Data;
+
+public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
- public class CleanDatabaseScheduledTask : ILibraryPostScanTask
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ public CleanDatabaseScheduledTask(
+ ILibraryManager libraryManager,
+ ILogger<CleanDatabaseScheduledTask> logger,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<CleanDatabaseScheduledTask> _logger;
- private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _dbProvider = dbProvider;
+ }
- public CleanDatabaseScheduledTask(
- ILibraryManager libraryManager,
- ILogger<CleanDatabaseScheduledTask> logger,
- IDbContextFactory<JellyfinDbContext> dbProvider)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _dbProvider = dbProvider;
- }
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
+ }
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
- await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
- }
+ HasDeadParentId = true
+ });
- private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
- {
- var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
- {
- HasDeadParentId = true
- });
+ var numComplete = 0;
+ var numItems = itemIds.Count + 1;
- var numComplete = 0;
- var numItems = itemIds.Count + 1;
+ _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
- _logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
+ foreach (var itemId in itemIds)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- foreach (var itemId in itemIds)
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is not null)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- var item = _libraryManager.GetItemById(itemId);
+ _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
- if (item is not null)
+ _libraryManager.DeleteItem(item, new DeleteOptions
{
- _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
-
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- });
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= numItems;
- progress.Report(percent * 100);
+ DeleteFileLocation = false
+ });
}
- var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
- await using (context.ConfigureAwait(false))
+ numComplete++;
+ double percent = numComplete;
+ percent /= numItems;
+ progress.Report(percent * 100);
+ }
+
+ var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await using (transaction.ConfigureAwait(false))
{
- var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
- await using (transaction.ConfigureAwait(false))
- {
- await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
- }
+ await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
-
- progress.Report(100);
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 4fef6ea01..b0003ed41 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library
private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
+ private readonly IPathManager _pathManager;
/// <summary>
/// The _root folder sync lock.
@@ -114,7 +115,8 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
- /// <param name="peopleRepository">The People Repository.</param>
+ /// <param name="peopleRepository">The people repository.</param>
+ /// <param name="pathManager">The path manager.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -131,7 +133,8 @@ namespace Emby.Server.Implementations.Library
IImageProcessor imageProcessor,
NamingOptions namingOptions,
IDirectoryService directoryService,
- IPeopleRepository peopleRepository)
+ IPeopleRepository peopleRepository,
+ IPathManager pathManager)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -149,6 +152,7 @@ namespace Emby.Server.Implementations.Library
_cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
+ _pathManager = pathManager;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -201,33 +205,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
+ private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
+ private IIntroProvider[] IntroProviders { get; set; } = [];
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
+ private IItemResolver[] EntityResolvers { get; set; } = [];
- private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
+ private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
+ private IBaseItemComparer[] Comparers { get; set; } = [];
public bool IsScanRunning { get; private set; }
@@ -360,7 +364,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
- : Array.Empty<BaseItem>();
+ : [];
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@@ -466,14 +470,28 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
- private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ {
+ var list = GetInternalMetadataPaths(item);
+ foreach (var child in children)
+ {
+ list.AddRange(GetInternalMetadataPaths(child));
+ }
+
+ return list;
+ }
+
+ private List<string> GetInternalMetadataPaths(BaseItem item)
{
var list = new List<string>
{
item.GetInternalMetadataPath()
};
- list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
+ if (item is Video video)
+ {
+ list.Add(_pathManager.GetTrickplayDirectory(video));
+ }
return list;
}
@@ -594,7 +612,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
- files = Array.Empty<FileSystemMetadata>();
+ files = [];
}
else
{
@@ -1345,6 +1363,21 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemList(query);
}
+ public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff)
+ {
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
+ }
+
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
{
if (query.User is not null)
@@ -1449,7 +1482,7 @@ namespace Emby.Server.Implementations.Library
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
- query.AncestorIds = Array.Empty<Guid>();
+ query.AncestorIds = [];
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
@@ -1569,7 +1602,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
if (!view.ParentId.IsEmpty())
@@ -1580,7 +1613,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
// Handle grouping
@@ -1595,7 +1628,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
- return Array.Empty<Guid>();
+ return [];
}
if (item is CollectionFolder collectionFolder)
@@ -1609,7 +1642,7 @@ namespace Emby.Server.Implementations.Library
return new[] { topParent.Id };
}
- return Array.Empty<Guid>();
+ return [];
}
/// <summary>
@@ -1653,7 +1686,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error getting intros");
- return Enumerable.Empty<IntroInfo>();
+ return [];
}
}
@@ -2480,8 +2513,11 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public int? GetSeasonNumberFromPath(string path)
- => SeasonPathParser.Parse(path, true, true).SeasonNumber;
+ public int? GetSeasonNumberFromPath(string path, Guid? parentId)
+ {
+ var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
+ return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
+ }
/// <inheritdoc />
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
@@ -2880,7 +2916,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
- await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
+ await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 8098199a7..3e71b2fcd 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -783,9 +783,13 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(id);
- // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
- var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
- return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
+ var info = GetLiveStreamInfo(id);
+ if (info is null)
+ {
+ return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
+ }
+
+ return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
}
public ILiveStream GetLiveStreamInfo(string id)
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index ea223e3ec..6791e3ca9 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ // Sort in the following order: Default > No tag > Forced
var sortedStreams = streams
.Where(i => i.Type == MediaStreamType.Subtitle)
.OrderByDescending(x => x.IsExternal)
- .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- .ThenByDescending(x => x.IsForced)
.ThenByDescending(x => x.IsDefault)
- .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+ .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+ .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
+ .ThenByDescending(x => x.IsForced)
.ToList();
MediaStream? stream = null;
+
if (mode == SubtitlePlaybackMode.Default)
{
- // Load subtitles according to external, forced and default flags.
- stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ // Load subtitles according to external, default and forced flags.
+ stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
- // If no subtitles of preferred language available, use default behaviour.
+ // If no subtitles of preferred language available, use none.
+ // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
- sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
}
else
{
- // Respect forced flag.
- stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+ stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
- stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
- sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
+ stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
+ BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // Only load subtitles that are flagged forced.
- stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+ // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+ stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
return stream?.Index;
@@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
- filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
+ // Load subtitles according to external, default, and forced flags.
+ filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
.ToList();
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Prefer smart logic over embedded metadata
+ // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
+ filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
.ToList();
}
+ else
+ {
+ filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
+ }
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // Always load the most suitable full subtitles
- filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
+ // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
+ filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
+ .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // Always load the most suitable full subtitles
- filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
+ // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+ filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
- // Load forced subs if we have found no suitable full subtitles
- var iterStreams = filteredStreams is null || filteredStreams.Count == 0
- ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- : filteredStreams;
+ // If filteredStreams is null, initialize it as an empty list to avoid null reference errors
+ filteredStreams ??= new List<MediaStream>();
- foreach (var stream in iterStreams)
+ foreach (var stream in filteredStreams)
{
stream.Score = GetStreamScore(stream, preferredLanguages);
}
}
+ private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
+ {
+ // If preferredLanguages is empty, treat it as "any language" (wildcard)
+ return preferredLanguages.Count == 0 ||
+ preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsLanguageUndefined(string language)
+ {
+ // Check for null, empty, or known placeholders
+ return string.IsNullOrEmpty(language) ||
+ language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
+ {
+ return sortedStreams
+ .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
+ .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
+ .ThenByDescending(s => IsLanguageUndefined(s.Language))
+ .ToList();
+ }
+
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
new file mode 100644
index 000000000..c910abadb
--- /dev/null
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -0,0 +1,36 @@
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IPathManager implementation.
+/// </summary>
+public class PathManager : IPathManager
+{
+ private readonly IServerConfigurationManager _config;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PathManager"/> class.
+ /// </summary>
+ /// <param name="config">The server configuration manager.</param>
+ public PathManager(
+ IServerConfigurationManager config)
+ {
+ _config = config;
+ }
+
+ /// <inheritdoc />
+ public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
+ {
+ var basePath = _config.ApplicationPaths.TrickplayPath;
+ var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
+
+ return saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(basePath, idString);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index abf2d0115..6cb63a28a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path;
- var seasonParserResult = SeasonPathParser.Parse(path, true, true);
+ var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
var season = new Season
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index fb48d7bf1..c81a0adb8 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
- if (IsSeasonFolder(child.FullName, isTvContentType))
+ if (IsSeasonFolder(child.FullName, path, isTvContentType))
{
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="parentPath">The parentpath.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
- private static bool IsSeasonFolder(string path, bool isTvContentType)
+ private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
{
- var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+ var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue;
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 97aa0ca58..d5da04fb9 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -1,6 +1,6 @@
{
"Sync": "Сінхранізаваць",
- "Playlists": "Плэйлісты",
+ "Playlists": "Спісы прайгравання",
"Latest": "Апошні",
"LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
@@ -16,7 +16,7 @@
"Collections": "Калекцыі",
"Default": "Па змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
- "Folders": "Папкі",
+ "Folders": "Тэчкі",
"Favorites": "Абранае",
"External": "Знешні",
"Genres": "Жанры",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 55f266032..f3195f0ea 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -11,7 +11,7 @@
"Collections": "Συλλογές",
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
- "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}",
+ "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
@@ -27,8 +27,8 @@
"HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
"HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση",
- "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
- "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη",
+ "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
+ "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
"LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα",
@@ -40,7 +40,7 @@
"Movies": "Ταινίες",
"Music": "Μουσική",
"MusicVideos": "Μουσικά Βίντεο",
- "NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
+ "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε",
"NameSeasonNumber": "Κύκλος {0}",
"NameSeasonUnknown": "Άγνωστος Κύκλος",
"NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.",
@@ -54,7 +54,7 @@
"NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
"NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
"NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
- "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε",
+ "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε",
"NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
"NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
"NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
@@ -63,9 +63,9 @@
"Photos": "Φωτογραφίες",
"Playlists": "Λίστες αναπαραγωγής",
"Plugin": "Πρόσθετο",
- "PluginInstalledWithName": "{0} εγκαταστήθηκε",
- "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
- "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
+ "PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
+ "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
+ "PluginUpdatedWithName": "Το {0} ενημερώθηκε",
"ProviderValue": "Πάροχος: {0}",
"ScheduledTaskFailedWithName": "{0} αποτυχία",
"ScheduledTaskStartedWithName": "{0} ξεκίνησε",
@@ -96,7 +96,7 @@
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
"TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
"TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
- "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
+ "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων",
"TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
"TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
"TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
@@ -125,7 +125,7 @@
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
"External": "Εξωτερικό",
"HearingImpaired": "Με προβλήματα ακοής",
- "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
+ "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay",
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 0b595c2ca..42cce1096 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -122,5 +122,9 @@
"AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
"TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
"TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",
- "External": "Ekstera"
+ "External": "Ekstera",
+ "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
+ "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
+ "TaskAudioNormalization": "Normaligo Sonnivela",
+ "HearingImpaired": "Surda"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 879bf64b0..42ea5e0a4 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -1,6 +1,6 @@
{
"Albums": "Álbuns",
- "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
+ "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"Application": "Aplicação",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index c939a5e09..754a01329 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
}
// Fairly common for some users to have "Rated R" in their rating field
- rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
- rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
+ rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Trim();
// Use rating system matching the language
if (!string.IsNullOrEmpty(countryCode))
diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json
index 0a11b3e45..d92dc880b 100644
--- a/Emby.Server.Implementations/Localization/countries.json
+++ b/Emby.Server.Implementations/Localization/countries.json
@@ -336,7 +336,7 @@
"TwoLetterISORegionName": "IE"
},
{
- "DisplayName": "Islamic Republic of Pakistan",
+ "DisplayName": "Pakistan",
"Name": "PK",
"ThreeLetterISORegionName": "PAK",
"TwoLetterISORegionName": "PK"
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index daeb7fed8..9e780a49e 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Playlists
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (item is null)
{
- _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
+ _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId);
return;
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index cec4022cc..dc37c4f2a 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -344,6 +344,11 @@ namespace Emby.Server.Implementations.Session
/// <returns>Task.</returns>
private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime)
{
+ if (session is null)
+ {
+ return;
+ }
+
if (string.IsNullOrEmpty(info.MediaSourceId))
{
info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
@@ -676,6 +681,11 @@ namespace Emby.Server.Implementations.Session
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
{
+ if (session is null)
+ {
+ return null;
+ }
+
var item = session.FullNowPlayingItem;
if (item is not null && item.Id.Equals(itemId))
{
@@ -795,7 +805,11 @@ namespace Emby.Server.Implementations.Session
ArgumentNullException.ThrowIfNull(info);
- var session = GetSession(info.SessionId);
+ var session = GetSession(info.SessionId, false);
+ if (session is null)
+ {
+ return;
+ }
var libraryItem = info.ItemId.IsEmpty()
? null
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 39e751ca6..270c1b441 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request);
+ return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
}
if (limit.HasValue)
@@ -100,25 +100,9 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10;
}
- var items = _libraryManager
- .GetItemList(
- new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
- SeriesPresentationUniqueKey = presentationUniqueKey,
- Limit = limit,
- DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
- GroupBySeriesPresentationUniqueKey = true
- },
- parentsFolders.ToList())
- .Cast<Episode>()
- .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
- .Select(GetUniqueSeriesKey)
- .ToList();
-
- // Avoid implicitly captured closure
- var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
+ var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
+
+ var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
return GetResult(episodes, request);
}
@@ -134,36 +118,11 @@ namespace Emby.Server.Implementations.TV
.OrderByDescending(i => i.LastWatchedDate);
}
- // If viewing all next up for all series, remove first episodes
- // But if that returns empty, keep those first episodes (avoid completely empty view)
- var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty();
- var anyFound = false;
-
return allNextUp
- .Where(i =>
- {
- if (request.DisableFirstEpisode)
- {
- return i.LastWatchedDate != DateTime.MinValue;
- }
-
- if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
- {
- anyFound = true;
- return true;
- }
-
- return !anyFound && i.LastWatchedDate == DateTime.MinValue;
- })
.Select(i => i.GetEpisodeFunction())
.Where(i => i is not null)!;
}
- private static string GetUniqueSeriesKey(Episode episode)
- {
- return episode.SeriesPresentationUniqueKey;
- }
-
private static string GetUniqueSeriesKey(Series series)
{
return series.GetPresentationUniqueKey();
@@ -179,13 +138,13 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = true,
Limit = 1,
ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions
{
- Fields = new[] { ItemFields.SortName },
+ Fields = [ItemFields.SortName],
EnableImages = false
}
};
@@ -203,8 +162,8 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Episode],
+ OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
Limit = 1,
IsPlayed = includePlayed,
IsVirtualItem = false,
@@ -229,7 +188,7 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = includePlayed,
IsVirtualItem = false,
DtoOptions = dtoOptions
@@ -249,7 +208,7 @@ namespace Emby.Server.Implementations.TV
consideredEpisodes.Add(nextEpisode);
}
- var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
+ var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
.Cast<Episode>();
if (lastWatchedEpisode is not null)
{
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index a3b4c8700..1c0a6af79 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -698,6 +698,7 @@ public class LiveTvController : BaseJellyfinApiController
/// Gets recommended live tv epgs.
/// </summary>
/// <param name="userId">Optional. filter by user id.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
/// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
@@ -720,6 +721,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
[FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? isAiring,
[FromQuery] bool? hasAired,
@@ -744,6 +746,7 @@ public class LiveTvController : BaseJellyfinApiController
var query = new InternalItemsQuery(user)
{
IsAiring = isAiring,
+ StartIndex = startIndex,
Limit = limit,
HasAired = hasAired,
IsSeries = isSeries,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index df46c2dac..cc070244b 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -86,7 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool? enableUserData,
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool disableFirstEpisode = false,
+ [FromQuery][ParameterObsolete] bool disableFirstEpisode = false,
[FromQuery] bool enableResumable = true,
[FromQuery] bool enableRewatching = false)
{
@@ -109,7 +110,6 @@ public class TvShowsController : BaseJellyfinApiController
StartIndex = startIndex,
User = user,
EnableTotalRecordCount = enableTotalRecordCount,
- DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
EnableResumable = enableResumable,
EnableRewatching = enableRewatching
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 392b7de74..bea69b282 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -101,16 +101,23 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
- context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
- context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
- context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
- context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
- context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
- context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
+ context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
+ context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
+ context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
+ context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
+ context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
+ context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
+ context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
+ context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
+ context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
+ context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
+ context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
+ context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
+ context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.SaveChanges();
transaction.Commit();
}
@@ -255,6 +262,37 @@ public sealed class BaseItemRepository
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
}
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ ArgumentNullException.ThrowIfNull(filter.User);
+
+ using var context = _dbProvider.CreateDbContext();
+
+ var query = context.BaseItems
+ .AsNoTracking()
+ .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
+ .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
+ .Join(
+ context.UserData.AsNoTracking(),
+ i => new { UserId = filter.User.Id, ItemId = i.Id },
+ u => new { UserId = u.UserId, ItemId = u.ItemId },
+ (entity, data) => new { Item = entity, UserData = data })
+ .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
+ .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
+ .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
+ .OrderByDescending(g => g.LastPlayedDate)
+ .Select(g => g.Key!);
+
+ if (filter.Limit.HasValue)
+ {
+ query = query.Take(filter.Limit.Value);
+ }
+
+ return query.ToArray();
+ }
+
private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
// This whole block is needed to filter duplicate entries on request
@@ -925,25 +963,11 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext();
- var innerQuery = new InternalItemsQuery(filter.User)
- {
- ExcludeItemTypes = filter.ExcludeItemTypes,
- IncludeItemTypes = filter.IncludeItemTypes,
- MediaTypes = filter.MediaTypes,
- AncestorIds = filter.AncestorIds,
- ItemIds = filter.ItemIds,
- TopParentIds = filter.TopParentIds,
- ParentId = filter.ParentId,
- IsAiring = filter.IsAiring,
- IsMovie = filter.IsMovie,
- IsSports = filter.IsSports,
- IsKids = filter.IsKids,
- IsNews = filter.IsNews,
- IsSeries = filter.IsSeries
- };
- var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery);
+ var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
- query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
+ query = query.Where(e => e.Type == returnType);
+ // this does not seem to be nesseary but it does not make any sense why this isn't working.
+ // && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
if (filter.OrderBy.Count != 0
|| !string.IsNullOrEmpty(filter.SearchTerm))
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index 5d209b0af..6949ec1a8 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Trickplay;
@@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager
private readonly IImageEncoder _imageEncoder;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IApplicationPaths _appPaths;
+ private readonly IPathManager _pathManager;
private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
- private static readonly string[] _trickplayImgExtensions = { ".jpg" };
+ private static readonly string[] _trickplayImgExtensions = [".jpg"];
/// <summary>
/// Initializes a new instance of the <see cref="TrickplayManager"/> class.
@@ -53,6 +55,7 @@ public class TrickplayManager : ITrickplayManager
/// <param name="imageEncoder">The image encoder.</param>
/// <param name="dbProvider">The database provider.</param>
/// <param name="appPaths">The application paths.</param>
+ /// <param name="pathManager">The path manager.</param>
public TrickplayManager(
ILogger<TrickplayManager> logger,
IMediaEncoder mediaEncoder,
@@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager
IServerConfigurationManager config,
IImageEncoder imageEncoder,
IDbContextFactory<JellyfinDbContext> dbProvider,
- IApplicationPaths appPaths)
+ IApplicationPaths appPaths,
+ IPathManager pathManager)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
@@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager
_imageEncoder = imageEncoder;
_dbProvider = dbProvider;
_appPaths = appPaths;
+ _pathManager = pathManager;
}
/// <inheritdoc />
@@ -610,10 +615,7 @@ public class TrickplayManager : ITrickplayManager
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{
- var path = saveWithMedia
- ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
- : Path.Combine(item.GetInternalMetadataPath(), "trickplay");
-
+ var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia);
var subdirectory = string.Format(
CultureInfo.InvariantCulture,
"{0} - {1}x{2}",
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 30b453d60..f183bce10 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -94,7 +94,7 @@ public class MigrateLibraryDb : IMigrationRoutine
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
- ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems
+ ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName FROM TypedBaseItems
""";
dbContext.BaseItems.ExecuteDelete();
@@ -168,7 +168,6 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.UserData.ExecuteDelete();
var users = dbContext.Users.AsNoTracking().ToImmutableArray();
- var oldUserdata = new Dictionary<string, UserData>();
foreach (var entity in queryResult)
{
@@ -189,6 +188,8 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.UserData.Add(userData);
}
+ users.Clear();
+ legacyBaseItemWithUserKeys.Clear();
_logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
dbContext.SaveChanges();
@@ -225,11 +226,12 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.PeopleBaseItemMap.ExecuteDelete();
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
+ var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet();
foreach (SqliteDataReader reader in connection.Query(personsQuery))
{
var itemId = reader.GetGuid(0);
- if (!dbContext.BaseItems.Any(f => f.Id == itemId))
+ if (!baseItemIds.Contains(itemId))
{
_logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
continue;
@@ -261,12 +263,16 @@ public class MigrateLibraryDb : IMigrationRoutine
});
}
+ baseItemIds.Clear();
+
foreach (var item in peopleCache)
{
dbContext.Peoples.Add(item.Value.Person);
dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
}
+ peopleCache.Clear();
+
_logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count);
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
@@ -1029,6 +1035,16 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.MediaType = mediaType;
}
+ if (reader.TryGetString(index++, out var sortName))
+ {
+ entity.SortName = sortName;
+ }
+
+ if (reader.TryGetString(index++, out var cleanName))
+ {
+ entity.CleanName = cleanName;
+ }
+
var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys);
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
index c1a9e8894..f4ebac377 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -39,7 +39,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
}
/// <inheritdoc />
- public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
+ public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
/// <inheritdoc />
public string Name => "MoveTrickplayFiles";
@@ -89,6 +89,12 @@ public class MoveTrickplayFiles : IMigrationRoutine
{
_fileSystem.MoveDirectory(oldPath, newPath);
}
+
+ oldPath = GetNewOldTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false);
+ if (_fileSystem.DirectoryExists(oldPath))
+ {
+ _fileSystem.MoveDirectory(oldPath, newPath);
+ }
}
} while (previousCount == Limit);
@@ -101,4 +107,20 @@ public class MoveTrickplayFiles : IMigrationRoutine
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
}
+
+ private string GetNewOldTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
+ {
+ var path = saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ var subdirectory = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} - {1}x{2}",
+ width.ToString(CultureInfo.InvariantCulture),
+ tileWidth.ToString(CultureInfo.InvariantCulture),
+ tileHeight.ToString(CultureInfo.InvariantCulture));
+
+ return Path.Combine(path, subdirectory);
+ }
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index d0f2bafb4..d8684f6da 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -11,7 +11,9 @@ using Emby.Server.Implementations;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite;
@@ -44,6 +46,9 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json";
private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory();
+ private static SetupServer _setupServer = new();
+ private static CoreAppHost? _appHost;
+ private static IHost? _jellyfinHost = null;
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@@ -70,6 +75,7 @@ namespace Jellyfin.Server
{
_startTimestamp = Stopwatch.GetTimestamp();
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
+ await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
@@ -124,22 +130,23 @@ namespace Jellyfin.Server
if (_restartOnShutdown)
{
_startTimestamp = Stopwatch.GetTimestamp();
+ _setupServer = new SetupServer();
+ await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
}
} while (_restartOnShutdown);
}
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{
- using var appHost = new CoreAppHost(
- appPaths,
- _loggerFactory,
- options,
- startupConfig);
-
- IHost? host = null;
+ using CoreAppHost appHost = new CoreAppHost(
+ appPaths,
+ _loggerFactory,
+ options,
+ startupConfig);
+ _appHost = appHost;
try
{
- host = Host.CreateDefaultBuilder()
+ _jellyfinHost = Host.CreateDefaultBuilder()
.UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder =>
@@ -156,14 +163,17 @@ namespace Jellyfin.Server
.Build();
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
- appHost.ServiceProvider = host.Services;
+ appHost.ServiceProvider = _jellyfinHost.Services;
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try
{
- await host.StartAsync().ConfigureAwait(false);
+ await _setupServer.StopAsync().ConfigureAwait(false);
+ _setupServer.Dispose();
+ _setupServer = null!;
+ await _jellyfinHost.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
@@ -182,7 +192,7 @@ namespace Jellyfin.Server
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
- await host.WaitForShutdownAsync().ConfigureAwait(false);
+ await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false);
_restartOnShutdown = appHost.ShouldRestart;
}
catch (Exception ex)
@@ -203,7 +213,8 @@ namespace Jellyfin.Server
await databaseProvider.RunShutdownTask(shutdownSource.Token).ConfigureAwait(false);
}
- host?.Dispose();
+ _appHost = null;
+ _jellyfinHost?.Dispose();
}
}
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
new file mode 100644
index 000000000..9e2cf5bc8
--- /dev/null
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -0,0 +1,172 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+
+namespace Jellyfin.Server.ServerSetupApp;
+
+/// <summary>
+/// Creates a fake application pipeline that will only exist for as long as the main app is not started.
+/// </summary>
+public sealed class SetupServer : IDisposable
+{
+ private IHost? _startupServer;
+ private bool _disposed;
+
+ /// <summary>
+ /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
+ /// </summary>
+ /// <param name="networkManagerFactory">The networkmanager.</param>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="serverApplicationHost">The servers application host.</param>
+ /// <returns>A Task.</returns>
+ public async Task RunAsync(
+ Func<INetworkManager?> networkManagerFactory,
+ IApplicationPaths applicationPaths,
+ Func<IServerApplicationHost?> serverApplicationHost)
+ {
+ ThrowIfDisposed();
+ _startupServer = Host.CreateDefaultBuilder()
+ .UseConsoleLifetime()
+ .ConfigureServices(serv =>
+ {
+ serv.AddHealthChecks()
+ .AddCheck<SetupHealthcheck>("StartupCheck");
+ })
+ .ConfigureWebHostDefaults(webHostBuilder =>
+ {
+ webHostBuilder
+ .UseKestrel()
+ .Configure(app =>
+ {
+ app.UseHealthChecks("/health");
+
+ app.Map("/startup/logger", loggerRoute =>
+ {
+ loggerRoute.Run(async context =>
+ {
+ var networkManager = networkManagerFactory();
+ if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
+ return;
+ }
+
+ var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath)
+ .EnumerateFiles()
+ .OrderBy(f => f.CreationTimeUtc)
+ .FirstOrDefault()
+ ?.FullName;
+ if (logFilePath is not null)
+ {
+ await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false);
+ }
+ });
+ });
+
+ app.Map("/System/Info/Public", systemRoute =>
+ {
+ systemRoute.Run(async context =>
+ {
+ var jfApplicationHost = serverApplicationHost();
+
+ var retryCounter = 0;
+ while (jfApplicationHost is null && retryCounter < 5)
+ {
+ await Task.Delay(500).ConfigureAwait(false);
+ jfApplicationHost = serverApplicationHost();
+ retryCounter++;
+ }
+
+ if (jfApplicationHost is null)
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
+ context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
+ return;
+ }
+
+ var sysInfo = new PublicSystemInfo
+ {
+ Version = jfApplicationHost.ApplicationVersionString,
+ ProductName = jfApplicationHost.Name,
+ Id = jfApplicationHost.SystemId,
+ ServerName = jfApplicationHost.FriendlyName,
+ LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
+ StartupWizardCompleted = false
+ };
+
+ await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
+ });
+ });
+
+ app.Run((context) =>
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
+ context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
+ context.Response.WriteAsync("<p>Jellyfin Server still starting. Please wait.</p>");
+ var networkManager = networkManagerFactory();
+ if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
+ {
+ context.Response.WriteAsync("<p>You can download the current logfiles <a href='/startup/logger'>here</a>.</p>");
+ }
+
+ return Task.CompletedTask;
+ });
+ });
+ })
+ .Build();
+ await _startupServer.StartAsync().ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Stops the Setup server.
+ /// </summary>
+ /// <returns>A task. Duh.</returns>
+ public async Task StopAsync()
+ {
+ ThrowIfDisposed();
+ if (_startupServer is null)
+ {
+ throw new InvalidOperationException("Tried to stop a non existing startup server");
+ }
+
+ await _startupServer.StopAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _startupServer?.Dispose();
+ }
+
+ private void ThrowIfDisposed()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ }
+
+ private class SetupHealthcheck : IHealthCheck
+ {
+ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
index 57c654667..7a8ab3236 100644
--- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs
+++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
@@ -84,5 +84,11 @@ namespace MediaBrowser.Common.Configuration
/// </summary>
/// <value>The magic string used for virtual path manipulation.</value>
string VirtualDataPath { get; }
+
+ /// <summary>
+ /// Gets the path used for storing trickplay files.
+ /// </summary>
+ /// <value>The trickplay path.</value>
+ string TrickplayPath { get; }
}
}
diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs
index 738096352..a498d6271 100644
--- a/MediaBrowser.Common/Net/NetworkUtils.cs
+++ b/MediaBrowser.Common/Net/NetworkUtils.cs
@@ -326,4 +326,23 @@ public static partial class NetworkUtils
return new IPAddress(BitConverter.GetBytes(broadCastIPAddress));
}
+
+ /// <summary>
+ /// Check if a subnet contains an address. This method also handles IPv4 mapped to IPv6 addresses.
+ /// </summary>
+ /// <param name="network">The <see cref="IPNetwork"/>.</param>
+ /// <param name="address">The <see cref="IPAddress"/>.</param>
+ /// <returns>Whether the supplied IP is in the supplied network.</returns>
+ public static bool SubnetContainsAddress(IPNetwork network, IPAddress address)
+ {
+ ArgumentNullException.ThrowIfNull(address);
+ ArgumentNullException.ThrowIfNull(network);
+
+ if (address.IsIPv4MappedToIPv6)
+ {
+ address = address.MapToIPv4();
+ }
+
+ return network.Contains(address);
+ }
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 0985cdacb..e99479ee1 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -139,11 +139,9 @@ namespace MediaBrowser.Controller.Entities.Audio
private static List<string> GetUserDataKeys(MusicArtist item)
{
var list = new List<string>();
- var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist);
-
- if (!string.IsNullOrEmpty(id))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId))
{
- list.Add("Artist-Musicbrainz-" + id);
+ list.Add("Artist-Musicbrainz-" + externalId);
}
list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index f8872e51f..e20679084 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -920,7 +920,7 @@ namespace MediaBrowser.Controller.Entities
// Remove from middle if surrounded by spaces
sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
- // Remove from end if followed by a space
+ // Remove from end if preceeded by a space
if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
{
sortable = sortable.Remove(sortable.Length - (search.Length + 1));
@@ -1776,7 +1776,6 @@ namespace MediaBrowser.Controller.Entities
public void AddStudio(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
-
var current = Studios;
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
@@ -1795,7 +1794,7 @@ namespace MediaBrowser.Controller.Entities
public void SetStudios(IEnumerable<string> names)
{
- Studios = names.Distinct().ToArray();
+ Studios = names.Trimmed().Distinct().ToArray();
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 4141b1712..24b1843ce 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
ArgumentNullException.ThrowIfNull(person);
ArgumentException.ThrowIfNullOrEmpty(person.Name);
+ person.Name = person.Name.Trim();
+
// Normalize
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index e3fbe8e4d..9dbac1e92 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{
- IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path);
+ IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId);
// If a change was made record it
if (IndexNumber.HasValue)
diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs
new file mode 100644
index 000000000..036889810
--- /dev/null
+++ b/MediaBrowser.Controller/IO/IPathManager.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Interface ITrickplayManager.
+/// </summary>
+public interface IPathManager
+{
+ /// <summary>
+ /// Gets the path to the trickplay image base folder.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
+ /// <returns>The absolute path.</returns>
+ public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false);
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 47b1cb16e..e4490bca3 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -426,8 +426,9 @@ namespace MediaBrowser.Controller.Library
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="parentId">The parent id.</param>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
- int? GetSeasonNumberFromPath(string path);
+ int? GetSeasonNumberFromPath(string path, Guid? parentId);
/// <summary>
/// Fills the missing episode numbers from path.
@@ -566,6 +567,15 @@ namespace MediaBrowser.Controller.Library
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
/// <summary>
+ /// Gets the list of series presentation keys for next up.
+ /// </summary>
+ /// <param name="query">The query to use.</param>
+ /// <param name="parents">Items to use for query.</param>
+ /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
+ /// <returns>List of series presentation keys.</returns>
+ IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff);
+
+ /// <summary>
/// Gets the items result.
/// </summary>
/// <param name="query">The query.</param>
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index afe2d833d..f1ed4fe27 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -60,6 +60,14 @@ public interface IItemRepository
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter);
/// <summary>
+ /// Gets the list of series presentation keys for next up.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
+ /// <returns>The list of keys.</returns>
+ IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff);
+
+ /// <summary>
/// Updates the inherited values.
/// </summary>
void UpdateInheritedValues();
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
index f451eac6d..584c3297a 100644
--- a/MediaBrowser.Controller/Providers/IExternalId.cs
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -32,12 +32,6 @@ namespace MediaBrowser.Controller.Providers
ExternalIdMediaType? Type { get; }
/// <summary>
- /// Gets the URL format string for this id.
- /// </summary>
- [Obsolete("Obsolete in 10.10, to be removed in 10.11")]
- string? UrlFormatString { get; }
-
- /// <summary>
/// Determines whether this id supports a given item type.
/// </summary>
/// <param name="item">The item.</param>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index e4ac59b67..119effe79 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -234,8 +234,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
item.CustomRating = reader.ReadNormalizedString();
break;
case "RunningTime":
- var runtimeText = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(runtimeText))
+ var runtimeText = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(runtimeText))
{
if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
@@ -253,7 +253,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
case "LockData":
- item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
+ item.IsLocked = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
break;
case "Network":
foreach (var name in reader.GetStringArray())
@@ -331,9 +331,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Rating":
case "IMDBrating":
{
- var rating = reader.ReadElementContentAsString();
+ var rating = reader.ReadNormalizedString();
- if (!string.IsNullOrWhiteSpace(rating))
+ if (!string.IsNullOrEmpty(rating))
{
// All external meta is saving this as '.' for decimal I believe...but just to be sure
if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
@@ -449,7 +449,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "OwnerUserId":
{
- var val = reader.ReadElementContentAsString();
+ var val = reader.ReadNormalizedString();
if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
{
@@ -464,7 +464,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Format3D":
{
- var val = reader.ReadElementContentAsString();
+ var val = reader.ReadNormalizedString();
if (item is Video video)
{
@@ -498,7 +498,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
string readerName = reader.Name;
if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
{
- var id = reader.ReadElementContentAsString();
+ var id = reader.ReadNormalizedString();
item.TrySetProviderId(providerIdValue, id);
}
else
@@ -580,7 +580,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Tagline":
- item.Tagline = reader.ReadNormalizedString();
+ var val = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(val))
+ {
+ item.Tagline = val;
+ }
+
break;
default:
reader.Skip();
@@ -842,7 +847,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
userId = reader.ReadNormalizedString();
break;
case "CanEdit":
- canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
+ canEdit = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
break;
default:
reader.Skip();
@@ -856,7 +861,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
// This is valid
- if (!string.IsNullOrWhiteSpace(userId) && Guid.TryParse(userId, out var guid))
+ if (!string.IsNullOrEmpty(userId) && Guid.TryParse(userId, out var guid))
{
return new PlaylistUserPermissions(guid, canEdit);
}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index c730f4cda..6b0fd9a14 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Xml;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -531,42 +532,44 @@ namespace MediaBrowser.MediaEncoding.Probing
private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
{
List<BaseItemPerson> peoples = new List<BaseItemPerson>();
+ var distinctPairs = pairs.Select(p => p.Value)
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .Trimmed()
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase))
{
- info.Studios = pairs.Select(p => p.Value)
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
+ info.Studios = distinctPairs.ToArray();
}
else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase))
{
- foreach (var pair in pairs)
+ foreach (var pair in distinctPairs)
{
peoples.Add(new BaseItemPerson
{
- Name = pair.Value,
+ Name = pair,
Type = PersonKind.Writer
});
}
}
else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase))
{
- foreach (var pair in pairs)
+ foreach (var pair in distinctPairs)
{
peoples.Add(new BaseItemPerson
{
- Name = pair.Value,
+ Name = pair,
Type = PersonKind.Producer
});
}
}
else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase))
{
- foreach (var pair in pairs)
+ foreach (var pair in distinctPairs)
{
peoples.Add(new BaseItemPerson
{
- Name = pair.Value,
+ Name = pair,
Type = PersonKind.Director
});
}
@@ -591,10 +594,10 @@ namespace MediaBrowser.MediaEncoding.Probing
switch (reader.Name)
{
case "key":
- name = reader.ReadElementContentAsString();
+ name = reader.ReadNormalizedString();
break;
case "string":
- value = reader.ReadElementContentAsString();
+ value = reader.ReadNormalizedString();
break;
default:
reader.Skip();
@@ -607,8 +610,8 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- if (string.IsNullOrWhiteSpace(name)
- || string.IsNullOrWhiteSpace(value))
+ if (string.IsNullOrEmpty(name)
+ || string.IsNullOrEmpty(value))
{
return null;
}
@@ -1453,7 +1456,7 @@ namespace MediaBrowser.MediaEncoding.Probing
var genres = new List<string>(info.Genres);
foreach (var genre in Split(genreVal, true))
{
- if (string.IsNullOrWhiteSpace(genre))
+ if (string.IsNullOrEmpty(genre))
{
continue;
}
diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
index 1f5163aa8..e7a309924 100644
--- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs
+++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace MediaBrowser.Model.Providers
{
/// <summary>
@@ -13,15 +11,11 @@ namespace MediaBrowser.Model.Providers
/// <param name="name">Name of the external id provider (IE: IMDB, MusicBrainz, etc).</param>
/// <param name="key">Key for this id. This key should be unique across all providers.</param>
/// <param name="type">Specific media type for this id.</param>
- /// <param name="urlFormatString">URL format string.</param>
- public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string? urlFormatString)
+ public ExternalIdInfo(string name, string key, ExternalIdMediaType? type)
{
Name = name;
Key = key;
Type = type;
-#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
- UrlFormatString = urlFormatString;
-#pragma warning restore CS0618 // Type or member is obsolete
}
/// <summary>
@@ -46,11 +40,5 @@ namespace MediaBrowser.Model.Providers
/// This can be used along with the <see cref="Name"/> to localize the external id on the client.
/// </remarks>
public ExternalIdMediaType? Type { get; set; }
-
- /// <summary>
- /// Gets or sets the URL format string.
- /// </summary>
- [Obsolete("Obsolete in 10.10, to be removed in 10.11")]
- public string? UrlFormatString { get; set; }
}
}
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 8dece28a0..aee720aa7 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -4,76 +4,69 @@ using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Model.Querying
+namespace MediaBrowser.Model.Querying;
+
+public class NextUpQuery
{
- public class NextUpQuery
+ public NextUpQuery()
{
- public NextUpQuery()
- {
- EnableImageTypes = Array.Empty<ImageType>();
- EnableTotalRecordCount = true;
- DisableFirstEpisode = false;
- NextUpDateCutoff = DateTime.MinValue;
- EnableResumable = false;
- EnableRewatching = false;
- }
-
- /// <summary>
- /// Gets or sets the user.
- /// </summary>
- /// <value>The user.</value>
- public required User User { get; set; }
+ EnableImageTypes = Array.Empty<ImageType>();
+ EnableTotalRecordCount = true;
+ NextUpDateCutoff = DateTime.MinValue;
+ EnableResumable = false;
+ EnableRewatching = false;
+ }
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public Guid? ParentId { get; set; }
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public required User User { get; set; }
- /// <summary>
- /// Gets or sets the series id.
- /// </summary>
- /// <value>The series id.</value>
- public Guid? SeriesId { get; set; }
+ /// <summary>
+ /// Gets or sets the parent identifier.
+ /// </summary>
+ /// <value>The parent identifier.</value>
+ public Guid? ParentId { get; set; }
- /// <summary>
- /// Gets or sets the start index. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- public int? StartIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the series id.
+ /// </summary>
+ /// <value>The series id.</value>
+ public Guid? SeriesId { get; set; }
- /// <summary>
- /// Gets or sets the maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- public int? Limit { get; set; }
+ /// <summary>
+ /// Gets or sets the start index. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
- /// <summary>
- /// Gets or sets the enable image types.
- /// </summary>
- /// <value>The enable image types.</value>
- public ImageType[] EnableImageTypes { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum number of items to return.
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
- public bool EnableTotalRecordCount { get; set; }
+ /// <summary>
+ /// Gets or sets the enable image types.
+ /// </summary>
+ /// <value>The enable image types.</value>
+ public ImageType[] EnableImageTypes { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether do disable sending first episode as next up.
- /// </summary>
- public bool DisableFirstEpisode { get; set; }
+ public bool EnableTotalRecordCount { get; set; }
- /// <summary>
- /// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
- /// </summary>
- public DateTime NextUpDateCutoff { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
+ /// </summary>
+ public DateTime NextUpDateCutoff { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether to include resumable episodes as next up.
- /// </summary>
- public bool EnableResumable { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to include resumable episodes as next up.
+ /// </summary>
+ public bool EnableResumable { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether getting rewatching next up list.
- /// </summary>
- public bool EnableRewatching { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether getting rewatching next up list.
+ /// </summary>
+ public bool EnableRewatching { get; set; }
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 778fbc712..e8994693d 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -1146,13 +1146,24 @@ namespace MediaBrowser.Providers.Manager
private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
{
- foreach (var person in target)
+ var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
+ var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
+
+ foreach (var name in targetByName.Select(g => g.Key))
{
- var normalizedName = person.Name.RemoveDiacritics();
- var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase));
+ var targetPeople = targetByName[name].ToArray();
+ var sourcePeople = sourceByName[name].ToArray();
+
+ if (sourcePeople.Length == 0)
+ {
+ continue;
+ }
- if (personInSource is not null)
+ for (int i = 0; i < targetPeople.Length; i++)
{
+ var person = targetPeople[i];
+ var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
+
foreach (var providerId in personInSource.ProviderIds)
{
person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
@@ -1162,6 +1173,16 @@ namespace MediaBrowser.Providers.Manager
{
person.ImageUrl = personInSource.ImageUrl;
}
+
+ if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
+ {
+ person.Role = personInSource.Role;
+ }
+
+ if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
+ {
+ person.SortOrder = personInSource.SortOrder;
+ }
}
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 8c45abe25..856f33b49 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -899,35 +899,10 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/>
public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
{
-#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
- var legacyExternalIdUrls = GetExternalIds(item)
- .Select(i =>
- {
- var urlFormatString = i.UrlFormatString;
- if (string.IsNullOrEmpty(urlFormatString)
- || !item.TryGetProviderId(i.Key, out var providerId))
- {
- return null;
- }
-
- return new ExternalUrl
- {
- Name = i.ProviderName,
- Url = string.Format(
- CultureInfo.InvariantCulture,
- urlFormatString,
- providerId)
- };
- })
- .OfType<ExternalUrl>();
-#pragma warning restore CS0618 // Type or member is obsolete
-
- var externalUrls = _externalUrlProviders
+ return _externalUrlProviders
.SelectMany(p => p
.GetExternalUrls(item)
.Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl }));
-
- return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name);
}
/// <inheritdoc/>
@@ -937,10 +912,7 @@ namespace MediaBrowser.Providers.Manager
.Select(i => new ExternalIdInfo(
name: i.ProviderName,
key: i.Key,
- type: i.Type,
-#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
- urlFormatString: i.UrlFormatString));
-#pragma warning restore CS0618 // Type or member is obsolete
+ type: i.Type));
}
/// <inheritdoc/>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 963b61151..916e2625b 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using ATL;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -175,11 +176,15 @@ namespace MediaBrowser.Providers.MediaInfo
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
}
- track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
- track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
- track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
- track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
- track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
+ // We should never use the property setter of the ATL.Track class.
+ // That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
+ // For example, setting the Year property will also set the Date property, which is not what we want here.
+ // To properly handle fallback values, we make a clone of those fields when valid.
+ var trackTitle = (string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title).Trim();
+ var trackAlbum = (string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album).Trim();
+ var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
+ var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
+ var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
@@ -193,11 +198,11 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var albumArtist in albumArtists)
{
- if (!string.IsNullOrEmpty(albumArtist))
+ if (!string.IsNullOrWhiteSpace(albumArtist))
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = albumArtist,
+ Name = albumArtist.Trim(),
Type = PersonKind.AlbumArtist
});
}
@@ -225,11 +230,11 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var performer in performers)
{
- if (!string.IsNullOrEmpty(performer))
+ if (!string.IsNullOrWhiteSpace(performer))
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = performer,
+ Name = performer.Trim(),
Type = PersonKind.Artist
});
}
@@ -237,11 +242,11 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var composer in track.Composer.Split(InternalValueSeparator))
{
- if (!string.IsNullOrEmpty(composer))
+ if (!string.IsNullOrWhiteSpace(composer))
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = composer,
+ Name = composer.Trim(),
Type = PersonKind.Composer
});
}
@@ -276,22 +281,22 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
+ if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle))
{
- audio.Name = track.Title;
+ audio.Name = trackTitle;
}
if (options.ReplaceAllMetadata)
{
- audio.Album = track.Album;
- audio.IndexNumber = track.TrackNumber;
- audio.ParentIndexNumber = track.DiscNumber;
+ audio.Album = trackAlbum;
+ audio.IndexNumber = trackTrackNumber;
+ audio.ParentIndexNumber = trackDiscNumber;
}
else
{
- audio.Album ??= track.Album;
- audio.IndexNumber ??= track.TrackNumber;
- audio.ParentIndexNumber ??= track.DiscNumber;
+ audio.Album ??= trackAlbum;
+ audio.IndexNumber ??= trackTrackNumber;
+ audio.ParentIndexNumber ??= trackDiscNumber;
}
if (track.Date.HasValue)
@@ -299,11 +304,12 @@ namespace MediaBrowser.Providers.MediaInfo
audio.PremiereDate = track.Date;
}
- if (track.Year.HasValue)
+ if (trackYear.HasValue)
{
- var year = track.Year.Value;
+ var year = trackYear.Value;
audio.ProductionYear = year;
+ // ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks.
if (!audio.PremiereDate.HasValue)
{
try
@@ -312,7 +318,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
catch (ArgumentOutOfRangeException ex)
{
- _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
+ _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear);
}
}
}
@@ -326,6 +332,8 @@ namespace MediaBrowser.Providers.MediaInfo
genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
}
+ genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+
audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
? genres
: audio.Genres;
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 16f3175d2..266e1861f 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
@@ -407,7 +408,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
video.Genres = Array.Empty<string>();
- foreach (var genre in data.Genres)
+ foreach (var genre in data.Genres.Trimmed())
{
video.AddGenre(genre);
}
@@ -516,9 +517,9 @@ namespace MediaBrowser.Providers.MediaInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = person.Name,
+ Name = person.Name.Trim(),
Type = person.Type,
- Role = person.Role
+ Role = person.Role.Trim()
});
}
diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
index a8d74aa0b..def0b13c0 100644
--- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
@@ -22,9 +22,6 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "https://www.imdb.com/title/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item)
{
// Supports images for tv movies
diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs
new file mode 100644
index 000000000..980bac102
--- /dev/null
+++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Movies;
+
+/// <summary>
+/// External URLs for IMDb.
+/// </summary>
+public class ImdbExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "IMDb";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ var baseUrl = "https://www.imdb.com/";
+ if (item.TryGetProviderId(MetadataProvider.Imdb, out var externalId))
+ {
+ if (item is Person)
+ {
+ yield return baseUrl + $"name/{externalId}";
+ }
+ else
+ {
+ yield return baseUrl + $"title/{externalId}";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
index 8151ab471..aa2b2fae9 100644
--- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
@@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string UrlFormatString => "https://www.imdb.com/name/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person;
}
}
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 25698d8cb..0bcc301cb 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -187,7 +187,7 @@ namespace MediaBrowser.Providers.Music
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = albumArtist,
+ Name = albumArtist.Trim(),
Type = PersonKind.AlbumArtist
});
}
@@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Music
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = artist,
+ Name = artist.Trim(),
Type = PersonKind.Artist
});
}
diff --git a/MediaBrowser.Providers/Music/ImvdbId.cs b/MediaBrowser.Providers/Music/ImvdbId.cs
index ed69f369c..b2c0b7019 100644
--- a/MediaBrowser.Providers/Music/ImvdbId.cs
+++ b/MediaBrowser.Providers/Music/ImvdbId.cs
@@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Music
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string? UrlFormatString => null;
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item)
=> item is MusicVideo;
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
index 138cfef19..622bb1dba 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
@@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicAlbum;
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs
new file mode 100644
index 000000000..01d284105
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb;
+
+/// <summary>
+/// External artist URLs for AudioDb.
+/// </summary>
+public class AudioDbAlbumExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "TheAudioDb Album";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out var externalId))
+ {
+ var baseUrl = "https://www.theaudiodb.com/";
+ switch (item)
+ {
+ case MusicAlbum:
+ yield return baseUrl + $"album/{externalId}";
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
index 8a516e1ce..d2eeb7f07 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
@@ -50,9 +49,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
- var id = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
-
- if (!string.IsNullOrWhiteSpace(id))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var id))
{
await AudioDbAlbumProvider.Current.EnsureInfo(id, cancellationToken).ConfigureAwait(false);
@@ -70,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
}
}
- return Enumerable.Empty<RemoteImageInfo>();
+ return [];
}
private List<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
index 8aceb48c0..3b5955b5b 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
@@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs
new file mode 100644
index 000000000..56b0d9bcb
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb;
+
+/// <summary>
+/// External artist URLs for AudioDb.
+/// </summary>
+public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "TheAudioDb Artist";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
+ {
+ var baseUrl = "https://www.theaudiodb.com/";
+ switch (item)
+ {
+ case MusicAlbum:
+ case Person:
+ yield return baseUrl + $"artist/{externalId}";
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
index 4e7757cd2..88730f34d 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
@@ -43,21 +42,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new ImageType[]
- {
+ return
+ [
ImageType.Primary,
ImageType.Logo,
ImageType.Banner,
ImageType.Backdrop
- };
+ ];
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
- var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist);
-
- if (!string.IsNullOrWhiteSpace(id))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var id))
{
await AudioDbArtistProvider.Current.EnsureArtistInfo(id, cancellationToken).ConfigureAwait(false);
@@ -75,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
}
}
- return Enumerable.Empty<RemoteImageInfo>();
+ return [];
}
private List<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
index 014481da2..fdfd330cd 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
@@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
index 787539104..5a39ec1cd 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
@@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
index 825fe32fa..f1fc4a137 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
@@ -20,8 +20,5 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
/// <inheritdoc />
- public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs
new file mode 100644
index 000000000..f4b3f4f8c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// External album artist URLs for MusicBrainz.
+/// </summary>
+public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "MusicBrainz Album Artist";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item is MusicAlbum)
+ {
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId))
+ {
+ yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
index b7d53984c..48784e0ec 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
@@ -20,8 +20,5 @@ public class MusicBrainzAlbumExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs
new file mode 100644
index 000000000..b9d3b4835
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// External album URLs for MusicBrainz.
+/// </summary>
+public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "MusicBrainz Album";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item is MusicAlbum)
+ {
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId))
+ {
+ yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
index b3f001618..bd5d67ed1 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
@@ -20,8 +20,5 @@ public class MusicBrainzArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs
new file mode 100644
index 000000000..ee5a597c6
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// External artist URLs for MusicBrainz.
+/// </summary>
+public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "MusicBrainz Artist";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId))
+ {
+ switch (item)
+ {
+ case MusicAlbum:
+ case Person:
+ yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}";
+
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
index a0a922293..470cdad66 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
@@ -20,8 +20,5 @@ public class MusicBrainzOtherArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs
index d2af62806..89d8b9b99 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs
@@ -20,8 +20,5 @@ public class MusicBrainzRecordingId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Recording;
/// <inheritdoc />
- public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
index 47b6d6963..c19b62abf 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
@@ -20,8 +20,5 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
/// <inheritdoc />
- public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs
new file mode 100644
index 000000000..dd0a939f7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// External release group URLs for MusicBrainz.
+/// </summary>
+public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "MusicBrainz Release Group";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item is MusicAlbum)
+ {
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId))
+ {
+ yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs
new file mode 100644
index 000000000..59e6f42b1
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// External track URLs for MusicBrainz.
+/// </summary>
+public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "MusicBrainz Track";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item is Audio)
+ {
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId))
+ {
+ yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
index cb4345660..6a7b6f541 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
@@ -20,8 +20,5 @@ public class MusicBrainzTrackId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
/// <inheritdoc />
- public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index de0da7f7b..ad9edb031 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -421,7 +421,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Director,
+ Name = result.Director.Trim(),
Type = PersonKind.Director
};
@@ -432,7 +432,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Writer,
+ Name = result.Writer.Trim(),
Type = PersonKind.Writer
};
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
index d453a4ff4..2076589d3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
@@ -21,9 +21,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item)
{
return item is Movie || item is MusicVideo || item is Trailer;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
index 6d6032e8f..9a1d872ec 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -21,9 +21,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item)
{
// Supports images for tv movies
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index eef08b251..9bb6507fe 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -234,7 +234,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var genres = movieResult.Genres;
- foreach (var genre in genres.Select(g => g.Name))
+ foreach (var genre in genres.Select(g => g.Name).Trimmed())
{
movie.AddGenre(genre);
}
@@ -254,7 +254,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
- Role = actor.Character,
+ Role = actor.Character.Trim(),
Type = PersonKind.Actor,
SortOrder = actor.Order
};
@@ -289,7 +289,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
- Role = person.Job,
+ Role = person.Job?.Trim(),
Type = type
};
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
index d26a70028..2c0787b15 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
@@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item)
{
return item is Person;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index c93dabb66..73c3b4f16 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -211,7 +211,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
metadataResult.AddPerson(new PersonInfo
{
Name = actor.Name.Trim(),
- Role = actor.Character,
+ Role = actor.Character.Trim(),
Type = PersonKind.Actor,
SortOrder = actor.Order
});
@@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
metadataResult.AddPerson(new PersonInfo
{
Name = guest.Name.Trim(),
- Role = guest.Character,
+ Role = guest.Character.Trim(),
Type = PersonKind.GuestStar,
SortOrder = guest.Order
});
@@ -249,7 +249,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
metadataResult.AddPerson(new PersonInfo
{
Name = person.Name.Trim(),
- Role = person.Job,
+ Role = person.Job?.Trim(),
Type = type
});
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 3f208b599..b0a1e00df 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -82,12 +82,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
for (var i = 0; i < cast.Count; i++)
{
+ var member = cast[i];
result.AddPerson(new PersonInfo
{
- Name = cast[i].Name.Trim(),
- Role = cast[i].Character,
+ Name = member.Name.Trim(),
+ Role = member.Character.Trim(),
Type = PersonKind.Actor,
- SortOrder = cast[i].Order
+ SortOrder = member.Order
});
}
}
@@ -108,7 +109,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.AddPerson(new PersonInfo
{
Name = person.Name.Trim(),
- Role = person.Job,
+ Role = person.Job?.Trim(),
Type = type
});
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
index 5f2d7909a..840cec984 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
@@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item)
{
return item is Series;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index e4062740f..9ace9c674 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -330,7 +330,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
- Role = actor.Character,
+ Role = actor.Character.Trim(),
Type = PersonKind.Actor,
SortOrder = actor.Order,
ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
@@ -368,7 +368,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
yield return new PersonInfo
{
Name = person.Name.Trim(),
- Role = person.Job,
+ Role = person.Job?.Trim(),
Type = type
};
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
new file mode 100644
index 000000000..bec800c03
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using TMDbLib.Objects.TvShows;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb;
+
+/// <summary>
+/// External URLs for TMDb.
+/// </summary>
+public class TmdbExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "TMDB";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ switch (item)
+ {
+ case Series:
+ if (item.TryGetProviderId(MetadataProvider.Tmdb, out var externalId))
+ {
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}";
+ }
+
+ break;
+ case Season season:
+ if (season.Series.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId))
+ {
+ var orderString = season.Series.DisplayOrder;
+ if (string.IsNullOrEmpty(orderString))
+ {
+ // Default order is airdate
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}";
+ }
+
+ if (Enum.TryParse<TvGroupType>(season.Series.DisplayOrder, out var order))
+ {
+ if (order.Equals(TvGroupType.OriginalAirDate))
+ {
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}";
+ }
+ }
+ }
+
+ break;
+ case Episode episode:
+ if (episode.Series.TryGetProviderId(MetadataProvider.Imdb, out seriesExternalId))
+ {
+ var orderString = episode.Series.DisplayOrder;
+ if (string.IsNullOrEmpty(orderString))
+ {
+ // Default order is airdate
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}";
+ }
+
+ if (Enum.TryParse<TvGroupType>(orderString, out var order))
+ {
+ if (order.Equals(TvGroupType.OriginalAirDate))
+ {
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}";
+ }
+ }
+ }
+
+ break;
+ case Movie:
+ if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId))
+ {
+ yield return TmdbUtils.BaseTmdbUrl + $"movie/{externalId}";
+ }
+
+ break;
+ case Person:
+ if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId))
+ {
+ yield return TmdbUtils.BaseTmdbUrl + $"person/{externalId}";
+ }
+
+ break;
+ case BoxSet:
+ if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId))
+ {
+ yield return TmdbUtils.BaseTmdbUrl + $"collection/{externalId}";
+ }
+
+ break;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
index 3cb18e424..8907d7744 100644
--- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
@@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.TV
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
-
- /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Series;
}
}
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs
new file mode 100644
index 000000000..52b0583e5
--- /dev/null
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.TV;
+
+/// <summary>
+/// External URLs for TMDb.
+/// </summary>
+public class Zap2ItExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "Zap2It";
+
+ /// <inheritdoc/>
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item.TryGetProviderId(MetadataProvider.Zap2It, out var externalId))
+ {
+ yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}";
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
index 2385e7048..440296f09 100644
--- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -55,12 +56,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var album = (MusicAlbum)item;
- foreach (var artist in album.Artists)
+ foreach (var artist in album.Artists.Trimmed().OrderBy(artist => artist))
{
writer.WriteElementString("artist", artist);
}
- foreach (var artist in album.AlbumArtists)
+ foreach (var artist in album.AlbumArtists.Trimmed().OrderBy(artist => artist))
{
writer.WriteElementString("albumartist", artist);
}
@@ -70,11 +71,20 @@ namespace MediaBrowser.XbmcMetadata.Savers
private void AddTracks(IEnumerable<BaseItem> tracks, XmlWriter writer)
{
- foreach (var track in tracks.OrderBy(i => i.ParentIndexNumber ?? 0).ThenBy(i => i.IndexNumber ?? 0))
+ foreach (var track in tracks
+ .OrderBy(i => i.ParentIndexNumber ?? 0)
+ .ThenBy(i => i.IndexNumber ?? 0)
+ .ThenBy(i => SortNameOrName(i))
+ .ThenBy(i => i.Name?.Trim()))
{
writer.WriteStartElement("track");
- if (track.IndexNumber.HasValue)
+ if (track.ParentIndexNumber.HasValue && track.ParentIndexNumber.Value != 0)
+ {
+ writer.WriteElementString("disc", track.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (track.IndexNumber.HasValue && track.IndexNumber.Value != 0)
{
writer.WriteElementString("position", track.IndexNumber.Value.ToString(CultureInfo.InvariantCulture));
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
index df72ff044..b5ba2d24f 100644
--- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Xml;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -69,7 +70,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
private void AddAlbums(IReadOnlyList<BaseItem> albums, XmlWriter writer)
{
- foreach (var album in albums)
+ foreach (var album in albums
+ .OrderBy(album => album.ProductionYear ?? 0)
+ .ThenBy(album => SortNameOrName(album))
+ .ThenBy(album => album.Name?.Trim()))
{
writer.WriteStartElement("album");
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 51c5a2080..4c8a54cc9 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -488,7 +488,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
var directors = people
.Where(i => i.IsType(PersonKind.Director))
- .Select(i => i.Name)
+ .Select(i => i.Name?.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(i => i)
.ToList();
foreach (var person in directors)
@@ -498,8 +500,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
var writers = people
.Where(i => i.IsType(PersonKind.Writer))
- .Select(i => i.Name)
+ .Select(i => i.Name?.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(i => i)
.ToList();
foreach (var person in writers)
@@ -512,7 +515,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("credits", person);
}
- foreach (var trailer in item.RemoteTrailers)
+ foreach (var trailer in item.RemoteTrailers.OrderBy(t => t.Url?.Trim()))
{
writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url));
}
@@ -544,16 +547,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
}
- var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
-
- if (!string.IsNullOrEmpty(tmdbCollection))
+ if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection))
{
writer.WriteElementString("collectionnumber", tmdbCollection);
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
}
- var imdb = item.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdb))
+ if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
{
if (item is Series)
{
@@ -570,16 +570,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
// Series xml saver already saves this
if (item is not Series)
{
- var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
- if (!string.IsNullOrEmpty(tvdb))
+ if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
{
writer.WriteElementString("tvdbid", tvdb);
writtenProviderIds.Add(MetadataProvider.Tvdb.ToString());
}
}
- var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
- if (!string.IsNullOrEmpty(tmdb))
+ if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
{
writer.WriteElementString("tmdbid", tmdb);
writtenProviderIds.Add(MetadataProvider.Tmdb.ToString());
@@ -660,22 +658,22 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("tagline", item.Tagline);
}
- foreach (var country in item.ProductionLocations)
+ foreach (var country in item.ProductionLocations.Trimmed().OrderBy(country => country))
{
writer.WriteElementString("country", country);
}
- foreach (var genre in item.Genres)
+ foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre))
{
writer.WriteElementString("genre", genre);
}
- foreach (var studio in item.Studios)
+ foreach (var studio in item.Studios.Trimmed().OrderBy(studio => studio))
{
writer.WriteElementString("studio", studio);
}
- foreach (var tag in item.Tags)
+ foreach (var tag in item.Tags.Trimmed().OrderBy(tag => tag))
{
if (item is MusicAlbum || item is MusicArtist)
{
@@ -687,64 +685,49 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
- var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist);
-
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
{
writer.WriteElementString("audiodbartistid", externalId);
writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString());
}
- externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum);
-
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId))
{
writer.WriteElementString("audiodbalbumid", externalId);
writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString());
}
- externalId = item.GetProviderId(MetadataProvider.Zap2It);
-
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId))
{
writer.WriteElementString("zap2itid", externalId);
writtenProviderIds.Add(MetadataProvider.Zap2It.ToString());
}
- externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbum);
-
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId))
{
writer.WriteElementString("musicbrainzalbumid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString());
}
- externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist);
-
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId))
{
writer.WriteElementString("musicbrainzalbumartistid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString());
}
- externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist);
-
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId))
{
writer.WriteElementString("musicbrainzartistid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString());
}
- externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
-
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId))
{
writer.WriteElementString("musicbrainzreleasegroupid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString());
}
- externalId = item.GetProviderId(MetadataProvider.TvRage);
- if (!string.IsNullOrEmpty(externalId))
+ if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId))
{
writer.WriteElementString("tvrageid", externalId);
writtenProviderIds.Add(MetadataProvider.TvRage.ToString());
@@ -752,7 +735,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (item.ProviderIds is not null)
{
- foreach (var providerKey in item.ProviderIds.Keys)
+ foreach (var providerKey in item.ProviderIds.Keys.OrderBy(providerKey => providerKey))
{
var providerId = item.ProviderIds[providerKey];
if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
@@ -764,7 +747,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
XmlConvert.VerifyName(tagName);
Logger.LogDebug("Saving custom provider tagname {0}", tagName);
- writer.WriteElementString(GetTagForProviderKey(providerKey), providerId);
+ writer.WriteElementString(tagName, providerId);
}
catch (ArgumentException)
{
@@ -785,7 +768,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
AddUserData(item, writer, userManager, userDataRepo, options);
- AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
+ if (item is not MusicAlbum && item is not MusicArtist)
+ {
+ AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
+ }
if (item is BoxSet folder)
{
@@ -797,6 +783,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var items = item.LinkedChildren
.Where(i => i.Type == LinkedChildType.Manual)
+ .OrderBy(i => i.Path?.Trim())
+ .ThenBy(i => i.LibraryItemId?.Trim())
.ToList();
foreach (var link in items)
@@ -839,7 +827,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager));
}
- foreach (var backdrop in item.GetImages(ImageType.Backdrop))
+ foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim()))
{
writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager));
}
@@ -916,7 +904,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath)
{
- foreach (var person in people)
+ foreach (var person in people
+ .OrderBy(person => person.SortOrder ?? 0)
+ .ThenBy(person => person.Name?.Trim()))
{
if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
{
@@ -1027,5 +1017,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
private string GetTagForProviderKey(string providerKey)
=> providerKey.ToLowerInvariant() + "id";
+
+ protected static string SortNameOrName(BaseItem item)
+ {
+ if (item == null)
+ {
+ return string.Empty;
+ }
+
+ if (item.SortName != null)
+ {
+ string trimmed = item.SortName.Trim();
+ if (trimmed.Length > 0)
+ {
+ return trimmed;
+ }
+ }
+
+ return (item.Name ?? string.Empty).Trim();
+ }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index e85e369d9..099537de7 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -91,16 +92,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
/// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
{
- var imdb = item.GetProviderId(MetadataProvider.Imdb);
-
- if (!string.IsNullOrEmpty(imdb))
+ if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
{
writer.WriteElementString("id", imdb);
}
if (item is MusicVideo musicVideo)
{
- foreach (var artist in musicVideo.Artists)
+ foreach (var artist in musicVideo.Artists.Trimmed().OrderBy(artist => artist))
{
writer.WriteElementString("artist", artist);
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
index 083f22e5d..1ac6768a1 100644
--- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
@@ -54,9 +54,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var series = (Series)item;
- var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
-
- if (!string.IsNullOrEmpty(tvdb))
+ if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
{
writer.WriteElementString("id", tvdb);
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 4b9677d9f..715cbf220 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text.RegularExpressions;
using ICU4N.Text;
@@ -123,5 +125,15 @@ namespace Jellyfin.Extensions
{
return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text);
}
+
+ /// <summary>
+ /// Ensures all strings are non-null and trimmed of leading an trailing blanks.
+ /// </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)
+ {
+ return values.Select(i => (i ?? string.Empty).Trim());
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
index b2b82332d..3a2c46369 100644
--- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
@@ -344,15 +344,12 @@ public class RecordingsMetadataManager
await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false);
}
- var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
-
- if (!string.IsNullOrEmpty(tmdbCollection))
+ if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
{
await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false);
}
- var imdb = item.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdb))
+ if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
{
if (!isSeriesEpisode)
{
@@ -365,8 +362,7 @@ public class RecordingsMetadataManager
lockData = false;
}
- var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
- if (!string.IsNullOrEmpty(tvdb))
+ if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
{
await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false);
@@ -374,8 +370,7 @@ public class RecordingsMetadataManager
lockData = false;
}
- var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
- if (!string.IsNullOrEmpty(tmdb))
+ if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
{
await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false);
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index 2fbcbf79c..6f6ee5146 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -673,10 +673,10 @@ public class NetworkManager : INetworkManager, IDisposable
{
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
// If left blank, all remote addresses will be allowed.
- if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
+ if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP))
{
// remoteAddressFilter is a whitelist or blacklist.
- var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
+ var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubnetContainsAddress(remoteNetwork, remoteIP));
if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|| (config.IsRemoteIPFilterBlacklist && matches == 0))
{
@@ -793,7 +793,7 @@ public class NetworkManager : INetworkManager, IDisposable
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
}
- bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
+ bool isExternal = !IsInLocalNetwork(source);
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
@@ -840,7 +840,7 @@ public class NetworkManager : INetworkManager, IDisposable
// (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in availableInterfaces)
{
- if (intf.Subnet.Contains(source))
+ if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
{
result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
@@ -868,21 +868,11 @@ public class NetworkManager : INetworkManager, IDisposable
{
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
{
- return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
+ return IsInLocalNetwork(subnet.Prefix);
}
- if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
- {
- foreach (var ept in addresses)
- {
- if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
- {
- return true;
- }
- }
- }
-
- return false;
+ return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
+ && addresses.Any(IsInLocalNetwork);
}
/// <summary>
@@ -917,6 +907,11 @@ public class NetworkManager : INetworkManager, IDisposable
return CheckIfLanAndNotExcluded(address);
}
+ /// <summary>
+ /// Check if the address is in the LAN and not excluded.
+ /// </summary>
+ /// <param name="address">The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address.</param>
+ /// <returns>Boolean indicates whether the address is in LAN.</returns>
private bool CheckIfLanAndNotExcluded(IPAddress address)
{
foreach (var lanSubnet in _lanSubnets)
@@ -956,7 +951,7 @@ public class NetworkManager : INetworkManager, IDisposable
{
// Only use matching internal subnets
// Prefer more specific (bigger subnet prefix) overrides
- validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source))
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
}
@@ -964,7 +959,7 @@ public class NetworkManager : INetworkManager, IDisposable
{
// Only use matching external subnets
// Prefer more specific (bigger subnet prefix) overrides
- validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source))
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
}
@@ -972,7 +967,7 @@ public class NetworkManager : INetworkManager, IDisposable
foreach (var data in validPublishedServerUrls)
{
// Get interface matching override subnet
- var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
+ var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubnetContainsAddress(data.Data.Subnet, x.Address));
if (intf?.Address is not null
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
@@ -1035,6 +1030,7 @@ public class NetworkManager : INetworkManager, IDisposable
if (isInExternalSubnet)
{
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
+ .Where(x => !IsLinkLocalAddress(x.Address))
.OrderBy(x => x.Index)
.ToList();
if (externalInterfaces.Count > 0)
@@ -1042,7 +1038,7 @@ public class NetworkManager : INetworkManager, IDisposable
// Check to see if any of the external bind interfaces are in the same subnet as the source.
// If none exists, this will select the first external interface if there is one.
bindAddress = externalInterfaces
- .OrderByDescending(x => x.Subnet.Contains(source))
+ .OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source))
.ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
@@ -1060,7 +1056,7 @@ public class NetworkManager : INetworkManager, IDisposable
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
// If none exists, this will select the first internal interface if there is one.
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
- .OrderByDescending(x => x.Subnet.Contains(source))
+ .OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source))
.ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
@@ -1104,7 +1100,7 @@ public class NetworkManager : INetworkManager, IDisposable
// (For systems with multiple network cards and/or multiple subnets)
foreach (var intf in extResult)
{
- if (intf.Subnet.Contains(source))
+ if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
{
result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
index 3a042df68..4c8ba58d0 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
@@ -6,32 +6,54 @@ namespace Jellyfin.Naming.Tests.TV;
public class SeasonPathParserTests
{
[Theory]
- [InlineData("/Drive/Season 1", 1, true)]
- [InlineData("/Drive/s1", 1, true)]
- [InlineData("/Drive/S1", 1, true)]
- [InlineData("/Drive/Season 2", 2, true)]
- [InlineData("/Drive/Season 02", 2, true)]
- [InlineData("/Drive/Seinfeld/S02", 2, true)]
- [InlineData("/Drive/Seinfeld/2", 2, true)]
- [InlineData("/Drive/Seinfeld - S02", 2, true)]
- [InlineData("/Drive/Season 2009", 2009, true)]
- [InlineData("/Drive/Season1", 1, true)]
- [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
- [InlineData("/Drive/Season 7 (2016)", 7, false)]
- [InlineData("/Drive/Staffel 7 (2016)", 7, false)]
- [InlineData("/Drive/Stagione 7 (2016)", 7, false)]
- [InlineData("/Drive/Season (8)", null, false)]
- [InlineData("/Drive/3.Staffel", 3, false)]
- [InlineData("/Drive/s06e05", null, false)]
- [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
- [InlineData("/Drive/extras", 0, true)]
- [InlineData("/Drive/specials", 0, true)]
- public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
+ [InlineData("/Drive/Season 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
+ [InlineData("/Drive/sæson 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
+ [InlineData("/Drive/series 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
+ [InlineData("/Drive/sezona 1", "/Drive", 1, true)]
+ [InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
+ [InlineData("/Drive/시즌 1", "/Drive", 1, true)]
+ [InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
+ [InlineData("/Drive/сезон 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Season 10", "/Drive", 10, true)]
+ [InlineData("/Drive/Season 100", "/Drive", 100, true)]
+ [InlineData("/Drive/s1", "/Drive", 1, true)]
+ [InlineData("/Drive/S1", "/Drive", 1, true)]
+ [InlineData("/Drive/Season 2", "/Drive", 2, true)]
+ [InlineData("/Drive/Season 02", "/Drive", 2, true)]
+ [InlineData("/Drive/Seinfeld/S02", "/Seinfeld", 2, true)]
+ [InlineData("/Drive/Seinfeld/2", "/Seinfeld", 2, true)]
+ [InlineData("/Drive/Seinfeld Season 2", "/Drive", null, false)]
+ [InlineData("/Drive/Season 2009", "/Drive", 2009, true)]
+ [InlineData("/Drive/Season1", "/Drive", 1, true)]
+ [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", "/The Wonder Years", 4, true)]
+ [InlineData("/Drive/Season 7 (2016)", "/Drive", 7, true)]
+ [InlineData("/Drive/Staffel 7 (2016)", "/Drive", 7, true)]
+ [InlineData("/Drive/Stagione 7 (2016)", "/Drive", 7, true)]
+ [InlineData("/Drive/Stargate SG-1/Season 1", "/Drive/Stargate SG-1", 1, true)]
+ [InlineData("/Drive/Stargate SG-1/Stargate SG-1 Season 1", "/Drive/Stargate SG-1", 1, true)]
+ [InlineData("/Drive/Season (8)", "/Drive", null, false)]
+ [InlineData("/Drive/3.Staffel", "/Drive", 3, true)]
+ [InlineData("/Drive/s06e05", "/Drive", null, false)]
+ [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)]
+ [InlineData("/Drive/extras", "/Drive", 0, true)]
+ [InlineData("/Drive/specials", "/Drive", 0, true)]
+ [InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)]
+ public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
{
- var result = SeasonPathParser.Parse(path, true, true);
+ var result = SeasonPathParser.Parse(path, parentPath, true, true);
Assert.Equal(result.SeasonNumber is not null, result.Success);
- Assert.Equal(result.SeasonNumber, seasonNumber);
+ Assert.Equal(seasonNumber, result.SeasonNumber);
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
}
}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
index db427308c..222e624aa 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
@@ -217,68 +217,58 @@ public class MediaInfoResolverTests
string file = "My.Video.srt";
data.Add(
file,
- new[]
- {
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
- },
- new[]
- {
+ ],
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
- });
+ ]);
// filename has metadata
file = "My.Video.Title1.default.forced.sdh.en.srt";
data.Add(
file,
- new[]
- {
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
- },
- new[]
- {
+ ],
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true)
- });
+ ]);
// single stream with metadata
file = "My.Video.mks";
data.Add(
file,
- new[]
- {
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true)
- },
- new[]
- {
- CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true)
- });
+ ],
+ [
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, false, true)
+ ]);
// stream wins for title/language, filename wins for flags when conflicting
file = "My.Video.Title2.default.forced.sdh.en.srt";
data.Add(
file,
- new[]
- {
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0)
- },
- new[]
- {
+ ],
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true, true)
- });
+ ]);
// multiple stream with metadata - filename flags ignored but other data filled in when missing from stream
file = "My.Video.Title3.default.forced.en.srt";
data.Add(
file,
- new[]
- {
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true),
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
- },
- new[]
- {
+ ],
+ [
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true),
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
- });
+ ]);
return data;
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 65f018ee3..cc67dbc39 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -1,6 +1,5 @@
using System;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Emby.Server.Implementations.Localization;
using MediaBrowser.Controller.Configuration;
@@ -116,6 +115,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
[InlineData("TV-MA", "US", 17)]
[InlineData("XXX", "asdf", 1000)]
[InlineData("Germany: FSK-18", "DE", 18)]
+ [InlineData("Rated : R", "US", 17)]
+ [InlineData("Rated: R", "US", 17)]
+ [InlineData("Rated R", "US", 17)]
+ [InlineData(" PG-13 ", "US", 13)]
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
{
var localizationManager = Setup(new ServerConfiguration()
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index f9126ce9b..a04b37f21 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -26,7 +26,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var providerManager = new Mock<IProviderManager>();
var imdbExternalId = new ImdbExternalId();
- var externalIdInfo = new ExternalIdInfo(imdbExternalId.ProviderName, imdbExternalId.Key, imdbExternalId.Type, imdbExternalId.UrlFormatString);
+ var externalIdInfo = new ExternalIdInfo(imdbExternalId.ProviderName, imdbExternalId.Key, imdbExternalId.Type);
providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
.Returns(new[] { externalIdInfo });
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 9c2655154..a71a08d8c 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -34,7 +34,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var providerManager = new Mock<IProviderManager>();
var tmdbExternalId = new TmdbMovieExternalId();
- var externalIdInfo = new ExternalIdInfo(tmdbExternalId.ProviderName, tmdbExternalId.Key, tmdbExternalId.Type, tmdbExternalId.UrlFormatString);
+ var externalIdInfo = new ExternalIdInfo(tmdbExternalId.ProviderName, tmdbExternalId.Key, tmdbExternalId.Type);
providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
.Returns(new[] { externalIdInfo });
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
index f815dfaa9..24e9b9fee 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
@@ -24,7 +24,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var providerManager = new Mock<IProviderManager>();
var musicBrainzArtist = new MusicBrainzArtistExternalId();
- var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type, "MusicBrainzServer");
+ var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type);
providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
.Returns(new[] { externalIdInfo });
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
index 78183d9ff..4d1956bde 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
@@ -24,7 +24,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var providerManager = new Mock<IProviderManager>();
var musicBrainzArtist = new MusicBrainzArtistExternalId();
- var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type, "MusicBrainzServer");
+ var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type);
providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
.Returns(new[] { externalIdInfo });