aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/automation.yml1
-rw-r--r--.github/workflows/codeql-analysis.yml10
-rw-r--r--.github/workflows/commands.yml14
-rw-r--r--.github/workflows/openapi.yml22
-rw-r--r--.github/workflows/repo-stale.yaml2
-rw-r--r--Directory.Packages.props24
-rw-r--r--Dockerfile1
-rw-r--r--Dockerfile.arm1
-rw-r--r--Dockerfile.arm641
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs20
-rw-r--r--Emby.Naming/Common/NamingOptions.cs6
-rw-r--r--Emby.Naming/Video/VideoResolver.cs3
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs24
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs7
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs12
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs18
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs13
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs11
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs32
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json84
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json21
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json3
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs80
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs24
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs1
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs43
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs5
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs10
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs13
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs16
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs4
-rw-r--r--Jellyfin.Data/Enums/PersonKind.cs97
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs30
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs23
-rw-r--r--MediaBrowser.Controller/Entities/PersonInfo.cs9
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs182
-rw-r--r--MediaBrowser.Controller/Providers/ImageRefreshOptions.cs3
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs17
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs5
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs27
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs6
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs10
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs47
-rw-r--r--MediaBrowser.Model/Dto/BaseItemPerson.cs3
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs3
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs14
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs7
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs40
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs21
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs25
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs15
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs3
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs11
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs13
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs4
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs29
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs4
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs7
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs9
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs3
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs3
86 files changed, 833 insertions, 484 deletions
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index 4b5571c77..47abce02a 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -19,6 +19,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
+ commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
project:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 6d87af538..73aafe273 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
+ uses: github/codeql-action/init@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
+ uses: github/codeql-action/autobuild@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
+ uses: github/codeql-action/analyze@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 75227c57b..236a11a13 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index aa2e0417f..c68679ced 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,18 +14,18 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-head
retention-days: 14
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -51,13 +51,13 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-base
retention-days: 14
@@ -76,12 +76,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-base
path: openapi-base
@@ -103,14 +103,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2
+ uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2.3.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -125,7 +125,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 7f6fcffed..f8428847b 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
+ - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 48c766edb..f37eaa11e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,32 +14,32 @@
<PackageVersion Include="BlurHashSharp" Version="1.2.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
- <PackageVersion Include="Diacritics" Version="3.3.14" />
+ <PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.5" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageVersion Include="libse" Version="3.6.10" />
+ <PackageVersion Include="libse" Version="3.6.11" />
<PackageVersion Include="LrcParser" Version="2023.308.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
diff --git a/Dockerfile b/Dockerfile
index f5f5787be..e51d285e1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM debian:stable-slim as app
diff --git a/Dockerfile.arm b/Dockerfile.arm
index bbb84a461..46a3e9b99 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 5572586ae..4f9d5e1fd 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index bea7a5a0d..f668dc829 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -10,6 +10,7 @@ using System.Text;
using System.Xml;
using Emby.Dlna.ContentDirectory;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
var types = new[]
{
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer,
- PersonType.Composer,
- "creator"
+ PersonKind.Director,
+ PersonKind.Writer,
+ PersonKind.Producer,
+ PersonKind.Composer,
+ PersonKind.Creator
};
// Seeing some LG models locking up due content with large lists of people
@@ -888,10 +889,13 @@ namespace Emby.Dlna.Didl
foreach (var actor in people)
{
- var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
- ?? PersonType.Actor;
+ var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
+ if (type == PersonKind.Unknown)
+ {
+ type = PersonKind.Actor;
+ }
- AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
+ AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
}
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index e9161a6b7..17d77837f 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -141,8 +141,7 @@ namespace Emby.Naming.Common
VideoFileStackingRules = new[]
{
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
- new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
- new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
+ new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@@ -157,7 +156,8 @@ namespace Emby.Naming.Common
@"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
- @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
+ @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
+ @"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
};
SubtitleFileExtensions = new[]
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 858e9dd2f..db5bfdbf9 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -87,8 +87,7 @@ namespace Emby.Naming.Video
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
- if (extraResult.ExtraType is null
- && TryCleanString(name, namingOptions, out var newName))
+ if (TryCleanString(name, namingOptions, out var newName))
{
name = newName;
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 1e3c4dea1..961e225e9 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -157,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
- public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
+ public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
- var channels = GetAllChannels()
- .Select(GetChannelEntity)
+ var channels = await GetAllChannelEntitiesAsync()
.OrderBy(i => i.SortName)
- .ToList();
+ .ToListAsync()
+ .ConfigureAwait(false);
if (query.IsRecordingsFolder.HasValue)
{
@@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
if (user is not null)
{
+ var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
channels = channels.Where(i =>
{
if (!i.IsVisible(user))
@@ -235,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
try
{
- return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
+ return GetChannelProvider(i).IsEnabledFor(userId);
}
catch
{
@@ -258,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
{
foreach (var item in all)
{
- RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
+ await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
}
}
@@ -269,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
- public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
+ public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
- var internalResult = GetChannelsInternal(query);
+ var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
var dtoOptions = new DtoOptions();
@@ -327,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
progress.Report(100);
}
- private Channel GetChannelEntity(IChannel channel)
+ private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
{
- return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
+ foreach (IChannel channel in GetAllChannels())
+ {
+ yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
+ }
}
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 3bf4d07c5..fcff5f98c 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -5540,7 +5540,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@Name" + index, person.Name);
statement.TryBind("@Role" + index, person.Role);
- statement.TryBind("@PersonType" + index, person.Type);
+ statement.TryBind("@PersonType" + index, person.Type.ToString());
statement.TryBind("@SortOrder" + index, person.SortOrder);
statement.TryBind("@ListOrder" + index, listIndex);
@@ -5569,9 +5569,10 @@ AND Type = @InternalPersonType)");
item.Role = role;
}
- if (reader.TryGetString(3, out var type))
+ if (reader.TryGetString(3, out var type)
+ && Enum.TryParse(type, true, out PersonKind personKind))
{
- item.Type = type;
+ item.Type = personKind;
}
if (reader.TryGetInt32(4, out var sortOrder))
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 45270de89..8b6682903 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -523,32 +523,32 @@ namespace Emby.Server.Implementations.Dto
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
.ThenBy(i =>
{
- if (i.IsType(PersonType.Actor))
+ if (i.IsType(PersonKind.Actor))
{
return 0;
}
- if (i.IsType(PersonType.GuestStar))
+ if (i.IsType(PersonKind.GuestStar))
{
return 1;
}
- if (i.IsType(PersonType.Director))
+ if (i.IsType(PersonKind.Director))
{
return 2;
}
- if (i.IsType(PersonType.Writer))
+ if (i.IsType(PersonKind.Writer))
{
return 3;
}
- if (i.IsType(PersonType.Producer))
+ if (i.IsType(PersonKind.Producer))
{
return 4;
}
- if (i.IsType(PersonType.Composer))
+ if (i.IsType(PersonKind.Composer))
{
return 4;
}
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 05d0a9b79..2e3988f9e 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -276,25 +276,31 @@ namespace Emby.Server.Implementations.EntryPoints
/// Libraries the update timer callback.
/// </summary>
/// <param name="state">The state.</param>
- private void LibraryUpdateTimerCallback(object state)
+ private async void LibraryUpdateTimerCallback(object state)
{
+ List<Folder> foldersAddedTo;
+ List<Folder> foldersRemovedFrom;
+ List<BaseItem> itemsUpdated;
+ List<BaseItem> itemsAdded;
+ List<BaseItem> itemsRemoved;
lock (_libraryChangedSyncLock)
{
// Remove dupes in case some were saved multiple times
- var foldersAddedTo = _foldersAddedTo
+ foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
- var foldersRemovedFrom = _foldersRemovedFrom
+ foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
- var itemsUpdated = _itemsUpdated
+ itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.DistinctBy(x => x.Id)
.ToList();
- SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
+ itemsAdded = _itemsAdded.ToList();
+ itemsRemoved = _itemsRemoved.ToList();
if (LibraryUpdateTimer is not null)
{
@@ -308,6 +314,8 @@ namespace Emby.Server.Implementations.EntryPoints
_foldersAddedTo.Clear();
_foldersRemovedFrom.Clear();
}
+
+ await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index e724618b3..d32759017 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private void UpdateTimerCallback(object? state)
+ private async void UpdateTimerCallback(object? state)
{
+ List<KeyValuePair<Guid, List<BaseItem>>> changes;
lock (_syncLock)
{
// Remove dupes in case some were saved multiple times
- var changes = _changedItems.ToList();
+ changes = _changedItems.ToList();
_changedItems.Clear();
- SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
-
if (_updateTimer is not null)
{
_updateTimer.Dispose();
_updateTimer = null;
}
}
+
+ await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
}
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
{
- foreach (var pair in changes)
+ foreach ((var key, var value) in changes)
{
- await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
+ await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index 0b255f673..b4791b945 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -4,6 +4,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
@@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Resolves a Path into a Video or Video subclass.
/// </summary>
- internal class ExtraResolver
+ internal class ExtraResolver : BaseVideoResolver<Video>
{
private readonly NamingOptions _namingOptions;
private readonly IItemResolver[] _trailerResolvers;
@@ -28,10 +29,16 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
/// <param name="directoryService">The directory service.</param>
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ : base(logger, namingOptions, directoryService)
{
_namingOptions = namingOptions;
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
- _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions, directoryService) };
+ _videoResolvers = new IItemResolver[] { this };
+ }
+
+ protected override Video Resolve(ItemResolveArgs args)
+ {
+ return ResolveVideo<Video>(args, true);
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 0e2d34d39..17f1d1905 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -111,10 +111,10 @@ namespace Emby.Server.Implementations.Library
if (query.IncludeExternalContent)
{
- var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
+ var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
{
UserId = query.UserId
- });
+ }).GetAwaiter().GetResult();
var channels = channelResult.Items;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 8edd8f66a..e7f4d2f4e 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -2032,7 +2032,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
var directors = people
- .Where(i => IsPersonType(i, PersonType.Director))
+ .Where(i => i.IsType(PersonKind.Director))
.Select(i => i.Name)
.ToList();
@@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
var writers = people
- .Where(i => IsPersonType(i, PersonType.Writer))
+ .Where(i => i.IsType(PersonKind.Writer))
.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -2122,10 +2122,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private static bool IsPersonType(PersonInfo person, string type)
- => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
- || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
-
private LiveTvProgram GetProgramInfoFromCache(string programId)
{
var query = new InternalItemsQuery
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 4003468d0..ee039ff0f 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
return 7;
}
- private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
+ private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user)
{
if (user is null)
{
return new QueryResult<BaseItem>();
}
- var folderIds = GetRecordingFolders(user, true)
- .Select(i => i.Id)
- .ToList();
+ var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false);
+ var folderIds = Array.ConvertAll(folders, x => x.Id);
var excludeItemTypes = new List<BaseItemKind>();
- if (folderIds.Count == 0)
+ if (folderIds.Length == 0)
{
return new QueryResult<BaseItem>();
}
@@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
{
MediaTypes = new[] { MediaType.Video },
Recursive = true,
- AncestorIds = folderIds.ToArray(),
+ AncestorIds = folderIds,
IsFolder = false,
IsVirtualItem = false,
Limit = limit,
@@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options)
+ public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options)
{
var user = query.UserId.Equals(default)
? null
@@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
RemoveFields(options);
- var internalResult = GetEmbyRecordings(query, options, user);
+ var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false);
var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
@@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
return _tvDtoService.GetInternalProgramId(externalId);
}
- public List<BaseItem> GetRecordingFolders(User user)
- {
- return GetRecordingFolders(user, false);
- }
+ /// <inheritdoc />
+ public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
+ => GetRecordingFoldersAsync(user, false);
- private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels)
+ private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels)
{
var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
.SelectMany(i => i.Locations)
@@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
.OrderBy(i => i.SortName)
.ToList();
- folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+ var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
{
UserId = user.Id,
IsRecordingsFolder = true,
RefreshLatestChannelItems = refreshChannels
- }).Items);
+ }).ConfigureAwait(false);
+
+ folders.AddRange(channels.Items);
- return folders.Cast<BaseItem>().ToList();
+ return folders.Cast<BaseItem>().ToArray();
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 046be7c5c..f2020e05f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var attributes = ParseExtInf(extInf, out string remaining);
extInf = remaining;
- if (attributes.TryGetValue("tvg-logo", out string value))
+ if (attributes.TryGetValue("tvg-logo", out string tvgLogo))
{
- channel.ImageUrl = value;
+ channel.ImageUrl = tvgLogo;
+ }
+ else if (attributes.TryGetValue("logo", out string logo))
+ {
+ channel.ImageUrl = logo;
}
if (attributes.TryGetValue("group-title", out string groupTitle))
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 1966f6968..26290df4d 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres",
- "CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
+ "CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}",
"Channels": "Canals",
"ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions",
@@ -16,65 +16,65 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum",
- "HeaderContinueWatching": "Continua Veient",
- "HeaderFavoriteAlbums": "Àlbums Preferits",
- "HeaderFavoriteArtists": "Artistes Predilectes",
- "HeaderFavoriteEpisodes": "Episodis Predilectes",
- "HeaderFavoriteShows": "Sèries Predilectes",
- "HeaderFavoriteSongs": "Cançons Predilectes",
- "HeaderLiveTV": "TV en Directe",
+ "HeaderContinueWatching": "Continuar veient",
+ "HeaderFavoriteAlbums": "Àlbums preferits",
+ "HeaderFavoriteArtists": "Artistes preferits",
+ "HeaderFavoriteEpisodes": "Episodis preferits",
+ "HeaderFavoriteShows": "Sèries preferides",
+ "HeaderFavoriteSongs": "Cançons preferides",
+ "HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació",
- "HeaderRecordingGroups": "Grups d'Enregistrament",
- "HomeVideos": "Vídeos Domèstics",
+ "HeaderRecordingGroups": "Grups d'enregistrament",
+ "HomeVideos": "Vídeos domèstics",
"Inherit": "Hereta",
- "ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
- "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
+ "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
+ "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
"LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps en funcionament: {0}",
- "Latest": "Darreres",
- "MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat",
- "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}",
+ "Latest": "Darrers",
+ "MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
+ "MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
"MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
"MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules",
"Music": "Música",
- "MusicVideos": "Vídeos Musicals",
+ "MusicVideos": "Videoclips",
"NameInstallFailed": "{0} instal·lació fallida",
"NameSeasonNumber": "Temporada {0}",
- "NameSeasonUnknown": "Temporada Desconeguda",
- "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.",
- "NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible",
- "NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada",
+ "NameSeasonUnknown": "Temporada desconeguda",
+ "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
+ "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
+ "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
"NotificationOptionInstallationFailed": "Instal·lació fallida",
"NotificationOptionNewLibraryContent": "Nou contingut afegit",
- "NotificationOptionPluginError": "Un connector ha fallat",
- "NotificationOptionPluginInstalled": "Connector instal·lat",
- "NotificationOptionPluginUninstalled": "Connector desinstal·lat",
- "NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada",
+ "NotificationOptionPluginError": "Un complement ha fallat",
+ "NotificationOptionPluginInstalled": "Complement instal·lat",
+ "NotificationOptionPluginUninstalled": "Complement desinstal·lat",
+ "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
"NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
"NotificationOptionTaskFailed": "Tasca programada fallida",
- "NotificationOptionUserLockedOut": "Usuari tancat",
- "NotificationOptionVideoPlayback": "Reproducció de video iniciada",
- "NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada",
+ "NotificationOptionUserLockedOut": "Usuari expulsat",
+ "NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
+ "NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada",
"Photos": "Fotos",
"Playlists": "Llistes de reproducció",
- "Plugin": "Connector",
+ "Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "{0} ha estat desinstal·lat",
"PluginUpdatedWithName": "{0} ha estat actualitzat",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
- "ScheduledTaskStartedWithName": "{0} iniciat",
+ "ScheduledTaskStartedWithName": "{0} s'ha iniciat",
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
"Shows": "Sèries",
"Songs": "Cançons",
- "StartupEmbyServerIsLoading": "El Servidor de Jellyfin està carregant. Si et plau, prova de nou ben aviat.",
+ "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}",
+ "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitzar",
"System": "Sistema",
"TvShows": "Sèries de TV",
@@ -82,11 +82,11 @@
"UserCreatedWithName": "S'ha creat l'usuari {0}",
"UserDeletedWithName": "L'usuari {0} ha estat eliminat",
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
- "UserLockedOutWithName": "L'usuari {0} ha sigut tancat",
+ "UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}",
"UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
- "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}",
+ "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
"ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
@@ -94,14 +94,14 @@
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
- "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'Internet.",
- "TaskRefreshChannels": "Actualitza Canals",
- "TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.",
+ "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
+ "TaskRefreshChannels": "Actualitza els canals",
+ "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja les transcodificacions",
- "TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.",
- "TaskUpdatePlugins": "Actualitza les extensions",
+ "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
+ "TaskUpdatePlugins": "Actualitza els connectors",
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
- "TaskRefreshPeople": "Actualitza Persones",
+ "TaskRefreshPeople": "Actualitza les persones",
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja els registres",
"TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
@@ -110,12 +110,12 @@
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
"TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
"TaskCleanCache": "Elimina arxius temporals",
- "TasksChannelsCategory": "Canals d'Internet",
+ "TasksChannelsCategory": "Canals d'internet",
"TasksApplicationCategory": "Aplicació",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manteniment",
"TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
- "TaskCleanActivityLog": "Buidar Registre d'Activitat",
+ "TaskCleanActivityLog": "Buidar el registre d'activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
"Default": "Per defecte",
@@ -124,5 +124,5 @@
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
"External": "Extern",
- "HearingImpaired": "Discapacitat Auditiva"
+ "HearingImpaired": "Discapacitat auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 99839ae6e..01b3e95fc 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -119,5 +119,9 @@
"Undefined": "Hindi tiyak",
"Forced": "Sapilitan",
"TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.",
- "TaskOptimizeDatabase": "I-optimize ang database"
+ "TaskOptimizeDatabase": "I-optimize ang database",
+ "HearingImpaired": "Bingi",
+ "TaskKeyframeExtractor": "Tagabunot ng Keyframe",
+ "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
+ "External": "External"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 7f616c35a..7b059c68e 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -37,8 +37,8 @@
"MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
"MessageServerConfigurationUpdated": "サーバー設定が更新されました",
"MixedContent": "ミックスコンテンツ",
- "Movies": "ムービー",
- "Music": "ミュージック",
+ "Movies": "映画",
+ "Music": "音楽",
"MusicVideos": "ミュージックビデオ",
"NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 3d54a5a95..b2293e4b6 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -39,7 +39,7 @@
"MixedContent": "Kandungan campuran",
"Movies": "Filem-filem",
"Music": "Muzik",
- "MusicVideos": "Video muzik",
+ "MusicVideos": "Video Muzik",
"NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Musim Tidak Diketahui",
@@ -55,7 +55,7 @@
"NotificationOptionPluginInstalled": "Plugin telah dipasang",
"NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang",
"NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang",
- "NotificationOptionServerRestartRequired": "",
+ "NotificationOptionServerRestartRequired": "Perlu mulakan semula server",
"NotificationOptionTaskFailed": "Kegagalan tugas berjadual",
"NotificationOptionUserLockedOut": "Pengguna telah dikunci",
"NotificationOptionVideoPlayback": "Ulangmain video bermula",
@@ -109,5 +109,20 @@
"TaskRefreshLibrary": "Imbas Perpustakaan Media",
"TaskRefreshChapterImagesDescription": "Membuat gambaran kecil untuk video yang mempunyai bab.",
"TaskRefreshChapterImages": "Ekstrak Gambar-gambar Bab",
- "TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem."
+ "TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem.",
+ "HearingImpaired": "Lemah Pendengaran",
+ "TaskRefreshPeopleDescription": "Kemas kini metadata untuk pelakon dan pengarah di dalam perpustakaan media.",
+ "TaskUpdatePluginsDescription": "Muat turun dan kemas kini plugin yang dikonfigurasi secara automatik.",
+ "TaskDownloadMissingSubtitlesDescription": "Cari sari kata yang hilang di internet, berdasarkan konfigurasi metadata.",
+ "TaskOptimizeDatabaseDescription": "Mampatkan pangkalan data dan potong ruang kosong. Pelaksanaan tugas ini selepas pengimbasan perpustakaan boleh membantu membaiki prestasi.",
+ "TaskRefreshChannels": "Segarkan Saluran-saluran",
+ "TaskUpdatePlugins": "Kemas kini plugin",
+ "TaskDownloadMissingSubtitles": "Muat turn sari kata yang tiada",
+ "TaskCleanTranscodeDescription": "Padam fail transkod yang lebih lama dari satu hari.",
+ "TaskRefreshChannelsDescription": "Segarkan maklumat saluran internet.",
+ "TaskCleanTranscode": "Bersihkan direktori transkod",
+ "External": "Luaran",
+ "TaskOptimizeDatabase": "Optimumkan pangkalan data",
+ "TaskKeyframeExtractor": "Ekstrak bingkai kunci",
+ "TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 383096f7e..4eb00d289 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
- "Collections": "Verzamelingen",
+ "Collections": "Collecties",
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
"DeviceOnlineWithName": "{0} is verbonden",
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
@@ -114,7 +114,7 @@
"TasksApplicationCategory": "Toepassing",
"TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud",
- "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.",
+ "TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 39229f45f..92e0d34ae 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -121,5 +121,6 @@
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"External": "Externo",
- "HearingImpaired": "Problemas auditivos"
+ "HearingImpaired": "Problemas auditivos",
+ "TaskKeyframeExtractor": "Extrator de quadro-chave"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 6e2a33fd5..166b71b4a 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -184,10 +184,19 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
{
- var ratings = GetParentalRatingsDictionary().Values.ToList();
+ // Use server default language for ratings
+ // Fall back to empty list if there are no parental ratings for that language
+ var ratings = GetParentalRatingsDictionary()?.Values.ToList()
+ ?? new List<ParentalRating>();
- // Add common ratings to ensure them being available for selection.
+ // Add common ratings to ensure them being available for selection
// Based on the US rating system due to it being the main source of rating in the metadata providers
+ // Unrated
+ if (!ratings.Any(x => x.Value is null))
+ {
+ ratings.Add(new ParentalRating("Unrated", null));
+ }
+
// Minimum rating possible
if (!ratings.Any(x => x.Value == 0))
{
@@ -237,36 +246,26 @@ namespace Emby.Server.Implementations.Localization
/// <summary>
/// Gets the parental ratings dictionary.
/// </summary>
+ /// <param name="countryCode">The optional two letter ISO language string.</param>
/// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
- private Dictionary<string, ParentalRating> GetParentalRatingsDictionary()
+ private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null)
{
- var countryCode = _configurationManager.Configuration.MetadataCountryCode;
-
- // Fall back to US ratings if no country code is specified or country code does not exist.
+ // Fallback to server default if no country code is specified.
if (string.IsNullOrEmpty(countryCode))
{
- countryCode = "us";
+ countryCode = _configurationManager.Configuration.MetadataCountryCode;
}
- return GetRatings(countryCode)
- ?? GetRatings("us")
- ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
- }
-
- /// <summary>
- /// Gets the ratings for a country.
- /// </summary>
- /// <param name="countryCode">The country code.</param>
- /// <returns>The ratings.</returns>
- private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
- {
- _allParentalRatings.TryGetValue(countryCode, out var countryValue);
+ if (_allParentalRatings.TryGetValue(countryCode, out var countryValue))
+ {
+ return countryValue;
+ }
- return countryValue;
+ return null;
}
/// <inheritdoc />
- public int? GetRatingLevel(string rating)
+ public int? GetRatingLevel(string rating, string? countryCode = null)
{
ArgumentException.ThrowIfNullOrEmpty(rating);
@@ -280,32 +279,51 @@ namespace Emby.Server.Implementations.Localization
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
- var ratingsDictionary = GetParentalRatingsDictionary();
-
- if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ // Use rating system matching the language
+ if (!string.IsNullOrEmpty(countryCode))
+ {
+ var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ {
+ return value.Value;
+ }
+ }
+ else
{
- return value.Value;
+ // Fall back to server default language for ratings check
+ // If it has no ratings, use the US ratings
+ var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ {
+ return value.Value;
+ }
}
- // If we don't find anything check all ratings systems
+ // If we don't find anything, check all ratings systems
foreach (var dictionary in _allParentalRatings.Values)
{
- if (dictionary.TryGetValue(rating, out value))
+ if (dictionary.TryGetValue(rating, out var value))
{
return value.Value;
}
}
- // Try splitting by : to handle "Germany: FSK 18"
+ // Try splitting by : to handle "Germany: FSK-18"
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
{
return GetRatingLevel(rating.AsSpan().RightPart(':').ToString());
}
- // Remove prefix country code to handle "DE-18"
+ // Handle prefix country code to handle "DE-18"
if (rating.Contains('-', StringComparison.OrdinalIgnoreCase))
{
- return GetRatingLevel(rating.AsSpan().RightPart('-').ToString());
+ var ratingSpan = rating.AsSpan();
+
+ // Extract culture from country prefix
+ var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
+
+ // Check rating system of culture
+ return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
}
return null;
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 9fe51f083..7732e32d0 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -62,23 +63,16 @@ namespace Emby.Server.Implementations.MediaEncoder
/// Determines whether [is eligible for chapter image extraction] [the specified video].
/// </summary>
/// <param name="video">The video.</param>
+ /// <param name="libraryOptions">The library options for the video.</param>
/// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
- private bool IsEligibleForChapterImageExtraction(Video video)
+ private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
{
if (video.IsPlaceHolder)
{
return false;
}
- var libraryOptions = _libraryManager.GetLibraryOptions(video);
- if (libraryOptions is not null)
- {
- if (!libraryOptions.EnableChapterImageExtraction)
- {
- return false;
- }
- }
- else
+ if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
{
return false;
}
@@ -99,7 +93,9 @@ namespace Emby.Server.Implementations.MediaEncoder
public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
{
- if (!IsEligibleForChapterImageExtraction(video))
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+
+ if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
{
extractImages = false;
}
@@ -179,6 +175,12 @@ namespace Emby.Server.Implementations.MediaEncoder
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
changesMade = true;
}
+ else if (libraryOptions?.EnableChapterImageExtraction != true)
+ {
+ // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
+ chapter.ImagePath = null;
+ changesMade = true;
+ }
}
if (saveChapters && changesMade)
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index abc203618..6ad6c4cbd 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -100,7 +100,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
EnableImages = false
},
SourceTypes = new SourceType[] { SourceType.Library },
- HasChapterImages = false,
IsVirtualItem = false
})
.OfType<Video>()
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index b5c4d8346..11c4ac376 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -52,7 +52,7 @@ public class ChannelsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the channels.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetChannels(
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannels(
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
@@ -61,7 +61,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] bool? isFavorite)
{
userId = RequestHelpers.GetUserId(User, userId);
- return _channelManager.GetChannels(new ChannelQuery
+ return await _channelManager.GetChannelsAsync(new ChannelQuery
{
Limit = limit,
StartIndex = startIndex,
@@ -69,7 +69,7 @@ public class ChannelsController : BaseJellyfinApiController
SupportsLatestItems = supportsLatestItems,
SupportsMediaDeletion = supportsMediaDeletion,
IsFavorite = isFavorite
- });
+ }).ConfigureAwait(false);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 16c77a923..42a7ac2b1 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
using Jellyfin.MediaEncoding.Hls.Playlist;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -1687,14 +1688,25 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += "-acodec " + audioCodec;
- if (state.OutputAudioBitrate.HasValue)
+ var audioBitrate = state.OutputAudioBitrate;
+ var audioChannels = state.OutputAudioChannels;
+
+ if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value / (audioChannels ?? 2));
+ if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ audioTranscodeParams += vbrParam;
+ }
+ else
+ {
+ audioTranscodeParams += " -ab " + audioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
}
- if (state.OutputAudioChannels.HasValue)
+ if (audioChannels.HasValue)
{
- audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
+ audioTranscodeParams += " -ac " + audioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
@@ -1708,11 +1720,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// dts, flac, opus and truehd are experimental in mp4 muxer
var strictArgs = string.Empty;
-
- if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
+ var actualOutputAudioCodec = state.ActualOutputAudioCodec;
+ if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
{
strictArgs = " -strict -2";
}
@@ -1746,10 +1758,17 @@ public class DynamicHlsController : BaseJellyfinApiController
}
var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value / (channels ?? 2));
+ if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ args += vbrParam;
+ }
+ else
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
}
if (state.OutputAudioSampleRate.HasValue)
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 9c7148241..ece053a9a 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -246,6 +246,11 @@ public class ItemUpdateController : BaseJellyfinApiController
episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
}
+ if (request.Height is not null && item is LiveTvChannel channel)
+ {
+ channel.Height = request.Height.Value;
+ }
+
item.Tags = request.Tags;
if (request.Taglines is not null)
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 96fc91f93..267ba4afb 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -252,7 +252,7 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("Recordings")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordings(
[FromQuery] string? channelId,
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
@@ -278,7 +278,7 @@ public class LiveTvController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- return _liveTvManager.GetRecordings(
+ return await _liveTvManager.GetRecordingsAsync(
new RecordingQuery
{
ChannelId = channelId,
@@ -299,7 +299,7 @@ public class LiveTvController : BaseJellyfinApiController
ImageTypeLimit = imageTypeLimit,
EnableImages = enableImages
},
- dtoOptions);
+ dtoOptions).ConfigureAwait(false);
}
/// <summary>
@@ -383,13 +383,13 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("Recordings/Folders")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
- var folders = _liveTvManager.GetRecordingFolders(user);
+ var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false);
var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 245239233..4486954c6 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -223,9 +224,17 @@ public class DynamicHlsHelper
sdrVideoUrl += "&AllowVideoStreamCopy=false";
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
- var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
- var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
+ var sdrOutputAudioBitrate = 0;
+ if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
+ }
+ else
+ {
+ sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
+ }
+ var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Provide a workaround for the case issue between flac and fLaC.
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 9b5a14c4d..9c91dcc6f 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -181,12 +181,18 @@ public static class StreamingHelpers
: GetOutputFileExtension(state, mediaSource);
}
- state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
-
- state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
-
- state.OutputAudioCodec = streamingRequest.AudioCodec;
+ var outputAudioCodec = streamingRequest.AudioCodec;
+ if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
+ {
+ state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
+ }
+ else
+ {
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
+ }
+ state.OutputAudioCodec = outputAudioCodec;
+ state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
if (state.VideoRequest is not null)
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 3eac81419..4a5e0ecd4 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -56,8 +56,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
base.Dispose(dispose);
}
- private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
+ private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
- SendData(true).GetAwaiter().GetResult();
+ await SendData(true).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs
new file mode 100644
index 000000000..10a805666
--- /dev/null
+++ b/Jellyfin.Data/Enums/PersonKind.cs
@@ -0,0 +1,97 @@
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// The person kind.
+/// </summary>
+public enum PersonKind
+{
+ /// <summary>
+ /// An unknown person kind.
+ /// </summary>
+ Unknown,
+
+ /// <summary>
+ /// A person whose profession is acting on the stage, in films, or on television.
+ /// </summary>
+ Actor,
+
+ /// <summary>
+ /// A person who supervises the actors and other staff in a film, play, or similar production.
+ /// </summary>
+ Director,
+
+ /// <summary>
+ /// A person who writes music, especially as a professional occupation.
+ /// </summary>
+ Composer,
+
+ /// <summary>
+ /// A writer of a book, article, or document. Can also be used as a generic term for music writer if there is a lack of specificity.
+ /// </summary>
+ Writer,
+
+ /// <summary>
+ /// A well-known actor or other performer who appears in a work in which they do not have a regular role.
+ /// </summary>
+ GuestStar,
+
+ /// <summary>
+ /// A person responsible for the financial and managerial aspects of the making of a film or broadcast or for staging a play, opera, etc.
+ /// </summary>
+ Producer,
+
+ /// <summary>
+ /// A person who directs the performance of an orchestra or choir.
+ /// </summary>
+ Conductor,
+
+ /// <summary>
+ /// A person who writes the words to a song or musical.
+ /// </summary>
+ Lyricist,
+
+ /// <summary>
+ /// A person who adapts a musical composition for performance.
+ /// </summary>
+ Arranger,
+
+ /// <summary>
+ /// An audio engineer who performed a general engineering role.
+ /// </summary>
+ Engineer,
+
+ /// <summary>
+ /// An engineer responsible for using a mixing console to mix a recorded track into a single piece of music suitable for release.
+ /// </summary>
+ Mixer,
+
+ /// <summary>
+ /// A person who remixed a recording by taking one or more other tracks, substantially altering them and mixing them together with other material.
+ /// </summary>
+ Remixer,
+
+ /// <summary>
+ /// A person who created the material.
+ /// </summary>
+ Creator,
+
+ /// <summary>
+ /// A person who was the artist.
+ /// </summary>
+ Artist,
+
+ /// <summary>
+ /// A person who was the album artist.
+ /// </summary>
+ AlbumArtist,
+
+ /// <summary>
+ /// A person who was the author.
+ /// </summary>
+ Author,
+
+ /// <summary>
+ /// A person who was the illustrator.
+ /// </summary>
+ Illustrator,
+}
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
index e392a3493..8eb27888a 100644
--- a/MediaBrowser.Controller/Channels/IChannelManager.cs
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -46,14 +46,14 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
+ Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query);
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
+ Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query);
/// <summary>
/// Gets the latest channel items.
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 8fe9cfa7f..a04f02bf9 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -893,16 +893,6 @@ namespace MediaBrowser.Controller.Entities
var sortable = Name.Trim().ToLowerInvariant();
- foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
- {
- sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
- }
-
- foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
- {
- sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
- }
-
foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
{
// Remove from beginning if a space follows
@@ -921,12 +911,22 @@ namespace MediaBrowser.Controller.Entities
}
}
+ foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+ {
+ sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
+ }
+
+ foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+ {
+ sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
+ }
+
return ModifySortChunks(sortable);
}
- internal static string ModifySortChunks(string name)
+ internal static string ModifySortChunks(ReadOnlySpan<char> name)
{
- void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
+ static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
{
if (isDigitChunk && chunk.Length < 10)
{
@@ -936,7 +936,7 @@ namespace MediaBrowser.Controller.Entities
builder.Append(chunk);
}
- if (name.Length == 0)
+ if (name.IsEmpty)
{
return string.Empty;
}
@@ -950,13 +950,13 @@ namespace MediaBrowser.Controller.Entities
var isDigit = char.IsDigit(name[i]);
if (isDigit != isDigitChunk)
{
- AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart, i - chunkStart));
+ AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
chunkStart = i;
isDigitChunk = isDigit;
}
}
- AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart));
+ AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
return builder.ToString().RemoveDiacritics();
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 7f8dc069c..5292bd772 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@@ -17,38 +18,38 @@ namespace MediaBrowser.Controller.Entities
// Normalize
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.GuestStar;
+ person.Type = PersonKind.GuestStar;
}
else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.Director;
+ person.Type = PersonKind.Director;
}
else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.Producer;
+ person.Type = PersonKind.Producer;
}
else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.Writer;
+ person.Type = PersonKind.Writer;
}
// If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes
- if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
+ if (person.Type == PersonKind.GuestStar)
{
- var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase));
+ var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type == PersonKind.Actor);
if (existing is not null)
{
- existing.Type = PersonType.GuestStar;
+ existing.Type = PersonKind.GuestStar;
MergeExisting(existing, person);
return;
}
}
- if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
+ if (person.Type == PersonKind.Actor)
{
// If the actor already exists without a role and we have one, fill it in
- var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)));
+ var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type == PersonKind.Actor || p.Type == PersonKind.GuestStar));
if (existing is null)
{
// Wasn't there - add it
@@ -68,8 +69,8 @@ namespace MediaBrowser.Controller.Entities
else
{
var existing = people.FirstOrDefault(p =>
- string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) &&
- string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase));
+ string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase)
+ && p.Type == person.Type);
// Check for dupes based on the combination of Name and Type
if (existing is null)
diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs
index 2b689ae7e..3df0b0b78 100644
--- a/MediaBrowser.Controller/Entities/PersonInfo.cs
+++ b/MediaBrowser.Controller/Entities/PersonInfo.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@@ -36,7 +37,7 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
- public string Type { get; set; }
+ public PersonKind Type { get; set; }
/// <summary>
/// Gets or sets the ascending sort order.
@@ -57,10 +58,6 @@ namespace MediaBrowser.Controller.Entities
return Name;
}
- public bool IsType(string type)
- {
- return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase)
- || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase);
- }
+ public bool IsType(PersonKind type) => Type == type || string.Equals(type.ToString(), Role, StringComparison.OrdinalIgnoreCase);
}
}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 46bdca302..3b6a16dee 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <returns>A recording.</returns>
- QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
+ Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options);
/// <summary>
/// Gets the timers.
@@ -308,6 +308,6 @@ namespace MediaBrowser.Controller.LiveTv
void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
- List<BaseItem> GetRecordingFolders(User user);
+ Task<BaseItem[]> GetRecordingFoldersAsync(User user);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 3e338e871..f7ee3c173 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -89,6 +89,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
+ public static readonly string[] LosslessAudioCodecs = new string[]
+ {
+ "alac",
+ "ape",
+ "flac",
+ "mlp",
+ "truehd",
+ "wavpack"
+ };
+
public EncodingHelper(
IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
@@ -620,6 +630,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return "flac";
}
+ if (string.Equals(codec, "dts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "dca";
+ }
+
return codec.ToLowerInvariant();
}
@@ -2050,9 +2065,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- // Video bitrate must fall within requested value
+ // Audio bitrate must fall within requested value
if (request.AudioBitRate.HasValue
- && audioStream.BitDepth.HasValue
+ && audioStream.BitRate.HasValue
&& audioStream.BitRate.Value > request.AudioBitRate.Value)
{
return false;
@@ -2116,14 +2131,20 @@ namespace MediaBrowser.Controller.MediaEncoding
private static double GetVideoBitrateScaleFactor(string codec)
{
+ // hevc & vp9 - 40% more efficient than h.264
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return .6;
}
+ // av1 - 50% more efficient than h.264
+ if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return .5;
+ }
+
return 1;
}
@@ -2131,7 +2152,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
- var scaleFactor = outputScaleFactor / inputScaleFactor;
+
+ // Don't scale the real bitrate lower than the requested bitrate
+ var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1);
if (bitrate <= 500000)
{
@@ -2153,56 +2176,96 @@ namespace MediaBrowser.Controller.MediaEncoding
return Convert.ToInt32(scaleFactor * bitrate);
}
- public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
+ public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream, int? outputAudioChannels)
{
- return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
+ return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream, outputAudioChannels);
}
- public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
+ public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream, int? outputAudioChannels)
{
if (audioStream is null)
{
return null;
}
- if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
+ var inputChannels = audioStream.Channels ?? 0;
+ var outputChannels = outputAudioChannels ?? 0;
+ var bitrate = audioBitRate ?? int.MaxValue;
+
+ if (string.IsNullOrEmpty(audioCodec)
+ || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
- return Math.Min(384000, audioBitRate.Value);
+ return (inputChannels, outputChannels) switch
+ {
+ (>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
+ (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
+ (> 0, _) => Math.Min(inputChannels * 128000, bitrate),
+ (_, _) => Math.Min(384000, bitrate)
+ };
}
- if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
+ if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ return (inputChannels, outputChannels) switch
{
- if ((audioStream.Channels ?? 0) >= 6)
- {
- return Math.Min(640000, audioBitRate.Value);
- }
+ (>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
+ (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
+ (> 0, _) => Math.Min(inputChannels * 136000, bitrate),
+ (_, _) => Math.Min(672000, bitrate)
+ };
+ }
- return Math.Min(384000, audioBitRate.Value);
- }
+ // Empty bitrate area is not allow on iOS
+ // Default audio bitrate to 128K per channel if we don't have codec specific defaults
+ // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
+ return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2);
+ }
- if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ public string GetAudioVbrModeParam(string encoder, int bitratePerChannel)
+ {
+ if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -vbr:a " + bitratePerChannel switch
{
- if ((audioStream.Channels ?? 0) >= 6)
- {
- return Math.Min(3584000, audioBitRate.Value);
- }
+ < 32000 => "1",
+ < 48000 => "2",
+ < 64000 => "3",
+ < 96000 => "4",
+ _ => "5"
+ };
+ }
- return Math.Min(1536000, audioBitRate.Value);
- }
+ if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 48000 => "8",
+ < 64000 => "6",
+ < 88000 => "4",
+ < 112000 => "2",
+ _ => "0"
+ };
}
- // Empty bitrate area is not allow on iOS
- // Default audio bitrate to 128K if it is not being requested
- // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
- return 128000;
+ if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 40000 => "0",
+ < 56000 => "2",
+ < 80000 => "4",
+ < 112000 => "6",
+ _ => "8"
+ };
+ }
+
+ return null;
}
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions)
@@ -5662,14 +5725,22 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var inputChannels = audioStream is null ? 6 : audioStream.Channels ?? 6;
+ var shiftAudioCodecs = new List<string>();
if (inputChannels >= 6)
{
- return;
+ // DTS and TrueHD are not supported by HLS
+ // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
+ shiftAudioCodecs.Add("dca");
+ shiftAudioCodecs.Add("truehd");
+ }
+ else
+ {
+ // Transcoding to 2ch ac3 or eac3 almost always causes a playback failure
+ // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
+ shiftAudioCodecs.Add("ac3");
+ shiftAudioCodecs.Add("eac3");
}
- // Transcoding to 2ch ac3 almost always causes a playback failure
- // Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
- var shiftAudioCodecs = new[] { "ac3", "eac3" };
if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
@@ -5911,10 +5982,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase))
{
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2));
+ if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ args += vbrParam;
+ }
+ else
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
}
if (state.OutputAudioSampleRate.HasValue)
@@ -5932,23 +6010,33 @@ namespace MediaBrowser.Controller.MediaEncoding
var audioTranscodeParams = new List<string>();
var bitrate = state.OutputAudioBitrate;
+ var channels = state.OutputAudioChannels;
+ var outputCodec = state.OutputAudioCodec;
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase))
{
- audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
+ var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value / (channels ?? 2));
+ if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ audioTranscodeParams.Add(vbrParam);
+ }
+ else
+ {
+ audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
+ }
}
- if (state.OutputAudioChannels.HasValue)
+ if (channels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
- if (!string.IsNullOrEmpty(state.OutputAudioCodec))
+ if (!string.IsNullOrEmpty(outputCodec))
{
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
- if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
// opus only supports specific sampling rates
var sampleRate = state.OutputAudioSampleRate;
diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
index fd73ed5f8..05b4d43a5 100644
--- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
@@ -1,6 +1,7 @@
#pragma warning disable CA1819, CS1591
using System;
+using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.Entities;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Providers
public bool ReplaceAllImages { get; set; }
- public ImageType[] ReplaceImages { get; set; }
+ public IReadOnlyList<ImageType> ReplaceImages { get; set; }
public bool IsAutomated { get; set; }
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index a01490c96..7eab00be4 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
@@ -371,7 +372,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Director":
{
- foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Director }))
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@@ -386,7 +387,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Writer":
{
- foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer }))
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@@ -413,7 +414,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
else
{
// Old-style piped string
- foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Actor }))
+ foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@@ -429,7 +430,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "GuestStars":
{
- foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.GuestStar }))
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@@ -1051,7 +1052,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
{
var name = string.Empty;
- var type = PersonType.Actor; // If type is not specified assume actor
+ var type = PersonKind.Actor; // If type is not specified assume actor
var role = string.Empty;
int? sortOrder = null;
@@ -1072,11 +1073,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Type":
{
var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- type = val;
- }
+ _ = Enum.TryParse(val, true, out type);
break;
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 1f9c562ba..a6fcdb9e1 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -374,8 +374,8 @@ namespace MediaBrowser.LocalMetadata.Savers
{
await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false);
- await writer.WriteElementStringAsync(null, "Type", null, person.Type).ConfigureAwait(false);
- await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false);
+ await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(false);
+ await writer.WriteElementStringAsync(null, "Role", null, person.Role.ToString()).ConfigureAwait(false);
if (person.SortOrder.HasValue)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 540d50bf1..3980353d1 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -25,11 +25,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"mpeg2video",
"mpeg4",
"msmpeg4",
- "dts",
+ "dca",
"ac3",
"aac",
"mp3",
"flac",
+ "truehd",
"h264_qsv",
"hevc_qsv",
"mpeg2_qsv",
@@ -59,10 +60,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"aac_at",
"libfdk_aac",
"ac3",
+ "dca",
"libmp3lame",
"libopus",
"libvorbis",
"flac",
+ "truehd",
"srt",
"h264_amf",
"hevc_amf",
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index cb482301f..dce3f0e39 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -9,6 +9,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -507,7 +508,7 @@ namespace MediaBrowser.MediaEncoding.Probing
peoples.Add(new BaseItemPerson
{
Name = pair.Value,
- Type = PersonType.Writer
+ Type = PersonKind.Writer
});
}
}
@@ -518,7 +519,7 @@ namespace MediaBrowser.MediaEncoding.Probing
peoples.Add(new BaseItemPerson
{
Name = pair.Value,
- Type = PersonType.Producer
+ Type = PersonKind.Producer
});
}
}
@@ -529,7 +530,7 @@ namespace MediaBrowser.MediaEncoding.Probing
peoples.Add(new BaseItemPerson
{
Name = pair.Value,
- Type = PersonType.Director
+ Type = PersonKind.Director
});
}
}
@@ -1163,7 +1164,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(composer, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Composer });
}
}
@@ -1171,7 +1172,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(conductor, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Conductor });
}
}
@@ -1179,7 +1180,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(lyricist, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Lyricist });
}
}
@@ -1195,7 +1196,7 @@ namespace MediaBrowser.MediaEncoding.Probing
people.Add(new BaseItemPerson
{
Name = match.Groups["name"].Value,
- Type = PersonType.Actor,
+ Type = PersonKind.Actor,
Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value)
});
}
@@ -1207,7 +1208,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(writer, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Writer });
}
}
@@ -1215,7 +1216,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(arranger, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Arranger });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Arranger });
}
}
@@ -1223,7 +1224,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(engineer, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Engineer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Engineer });
}
}
@@ -1231,7 +1232,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(mixer, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Mixer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Mixer });
}
}
@@ -1239,7 +1240,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(remixer, false))
{
- people.Add(new BaseItemPerson { Name = person, Type = PersonType.Remixer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Remixer });
}
}
@@ -1491,7 +1492,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
- .Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor })
+ .Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonKind.Actor })
.ToArray();
}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index f348d8417..f9f63f751 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -14,6 +14,7 @@ public class EncodingOptions
public EncodingOptions()
{
EnableFallbackFont = false;
+ EnableAudioVbr = false;
DownMixAudioBoost = 2;
DownMixStereoAlgorithm = DownMixStereoAlgorithms.None;
MaxMuxingQueueSize = 2048;
@@ -72,6 +73,11 @@ public class EncodingOptions
public bool EnableFallbackFont { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether audio VBR is enabled.
+ /// </summary>
+ public bool EnableAudioVbr { get; set; }
+
+ /// <summary>
/// Gets or sets the audio boost applied when downmixing audio.
/// </summary>
public double DownMixAudioBoost { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index c39162250..7cb07a2f7 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -243,16 +243,10 @@ namespace MediaBrowser.Model.Configuration
public bool AllowClientLogUpload { get; set; } = true;
/// <summary>
- /// Gets or sets the dummy chapters duration in seconds.
+ /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
/// </summary>
/// <value>The dummy chapters duration.</value>
- public int DummyChapterDuration { get; set; } = 300;
-
- /// <summary>
- /// Gets or sets the dummy chapter count.
- /// </summary>
- /// <value>The dummy chapter count.</value>
- public int DummyChapterCount { get; set; } = 100;
+ public int DummyChapterDuration { get; set; } = 0;
/// <summary>
/// Gets or sets the chapter image resolution.
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 6f99bbc13..db892a22c 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -23,6 +23,9 @@ namespace MediaBrowser.Model.Dlna
private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport;
+ private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" };
+ private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
+ private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@@ -44,32 +47,24 @@ namespace MediaBrowser.Model.Dlna
{
ValidateMediaOptions(options, false);
- var mediaSources = new List<MediaSourceInfo>();
+ var streams = new List<StreamInfo>();
foreach (var mediaSource in options.MediaSources)
{
- if (string.IsNullOrEmpty(options.MediaSourceId)
- || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+ if (!(string.IsNullOrEmpty(options.MediaSourceId)
+ || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)))
{
- mediaSources.Add(mediaSource);
+ continue;
}
- }
- var streams = new List<StreamInfo>();
- foreach (var mediaSourceInfo in mediaSources)
- {
- StreamInfo? streamInfo = GetOptimalAudioStream(mediaSourceInfo, options);
+ StreamInfo? streamInfo = GetOptimalAudioStream(mediaSource, options);
if (streamInfo is not null)
{
+ streamInfo.DeviceId = options.DeviceId;
+ streamInfo.DeviceProfileId = options.Profile.Id;
streams.Add(streamInfo);
}
}
- foreach (var stream in streams)
- {
- stream.DeviceId = options.DeviceId;
- stream.DeviceProfileId = options.Profile.Id;
- }
-
return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
}
@@ -399,7 +394,6 @@ namespace MediaBrowser.Model.Dlna
return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
- var playMethods = new List<PlayMethod>();
TranscodeReason transcodeReasons = 0;
// The profile describes what the device supports
@@ -810,6 +804,13 @@ namespace MediaBrowser.Model.Dlna
{
// Prefer matching video codecs
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
+
+ // Enforce HLS video codec restrictions
+ if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+ {
+ videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
+ }
+
var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
if (directVideoCodec is not null)
{
@@ -845,6 +846,20 @@ namespace MediaBrowser.Model.Dlna
// Prefer matching audio codecs, could do better here
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
+
+ // Enforce HLS audio codec restrictions
+ if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+ {
+ if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray();
+ }
+ else
+ {
+ audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray();
+ }
+ }
+
var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
playlistItem.AudioCodecs = audioCodecs;
if (directAudioStream is not null)
diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs
index 9c65a2308..d3bcf492d 100644
--- a/MediaBrowser.Model/Dto/BaseItemPerson.cs
+++ b/MediaBrowser.Model/Dto/BaseItemPerson.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Dto
@@ -33,7 +34,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
- public string Type { get; set; }
+ public PersonKind Type { get; set; }
/// <summary>
/// Gets or sets the primary image tag.
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index e00157dce..02a29e7fa 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -30,8 +30,9 @@ namespace MediaBrowser.Model.Globalization
/// Gets the rating level.
/// </summary>
/// <param name="rating">The rating.</param>
+ /// <param name="countryCode">The optional two letter ISO language string.</param>
/// <returns><see cref="int" /> or <c>null</c>.</returns>
- int? GetRatingLevel(string rating);
+ int? GetRatingLevel(string rating, string? countryCode = null);
/// <summary>
/// Gets the localized string.
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 5d59c4663..ba2d2db2f 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -273,7 +273,7 @@ namespace MediaBrowser.Providers.Manager
}
if (!refreshOptions.ReplaceAllImages &&
- refreshOptions.ReplaceImages.Length == 0 &&
+ refreshOptions.ReplaceImages.Count == 0 &&
ContainsImages(item, provider.GetSupportedImages(item).ToList(), savedOptions, backdropLimit))
{
return;
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 0605b0bd7..ebd653e34 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -26,6 +26,8 @@ namespace MediaBrowser.Providers.Manager
where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
where TIdType : ItemLookupInfo, new()
{
+ private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
+
protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
{
ServerConfigurationManager = serverConfigurationManager;
@@ -672,6 +674,8 @@ namespace MediaBrowser.Providers.Manager
}
var hasLocalMetadata = false;
+ var replaceImages = AllImageTypes.ToList();
+ var localImagesFound = false;
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{
@@ -698,6 +702,10 @@ namespace MediaBrowser.Providers.Manager
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+
+ // remove imagetype that has just been downloaded
+ replaceImages.Remove(remoteImage.Type);
+ localImagesFound = true;
}
catch (HttpRequestException ex)
{
@@ -705,6 +713,12 @@ namespace MediaBrowser.Providers.Manager
}
}
+ if (localImagesFound)
+ {
+ options.ReplaceAllImages = false;
+ options.ReplaceImages = replaceImages;
+ }
+
if (imageService.MergeImages(item, localItem.Images))
{
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 81ccd8653..d8122511e 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -404,12 +404,6 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- // Prevent owned items from reading the same local metadata file as their owner
- if (!item.OwnerId.Equals(default) && provider is ILocalMetadataProvider)
- {
- return false;
- }
-
if (includeDisabled)
{
return true;
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 19b594c1c..b8578c46f 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -163,7 +164,7 @@ namespace MediaBrowser.Providers.MediaInfo
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = albumArtist,
- Type = "AlbumArtist"
+ Type = PersonKind.AlbumArtist
});
}
@@ -173,7 +174,7 @@ namespace MediaBrowser.Providers.MediaInfo
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = performer,
- Type = "Artist"
+ Type = PersonKind.Artist
});
}
@@ -182,7 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = composer,
- Type = "Composer"
+ Type = PersonKind.Composer
});
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index e199db7f2..213639371 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -298,7 +298,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
options.MetadataRefreshMode == MetadataRefreshMode.Default)
{
- if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
+ if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
{
chapters = CreateDummyChapters(video);
}
@@ -662,39 +662,39 @@ namespace MediaBrowser.Providers.MediaInfo
private ChapterInfo[] CreateDummyChapters(Video video)
{
var runtime = video.RunTimeTicks ?? 0;
- long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
- if (runtime < 0)
+ // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
+ if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
- "{0} has invalid runtime of {1}",
+ "{0} has an invalid runtime of {1} minutes",
video.Name,
- runtime));
+ TimeSpan.FromTicks(runtime).Minutes));
}
- if (runtime < dummyChapterDuration)
+ long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
+ if (runtime > dummyChapterDuration)
{
- return Array.Empty<ChapterInfo>();
- }
+ int chapterCount = (int)(runtime / dummyChapterDuration);
+ var chapters = new ChapterInfo[chapterCount];
- // Limit the chapters just in case there's some incorrect metadata here
- int chapterCount = (int)Math.Min(runtime / dummyChapterDuration, _config.Configuration.DummyChapterCount);
- var chapters = new ChapterInfo[chapterCount];
-
- long currentChapterTicks = 0;
- for (int i = 0; i < chapterCount; i++)
- {
- chapters[i] = new ChapterInfo
+ long currentChapterTicks = 0;
+ for (int i = 0; i < chapterCount; i++)
{
- StartPositionTicks = currentChapterTicks
- };
+ chapters[i] = new ChapterInfo
+ {
+ StartPositionTicks = currentChapterTicks
+ };
+
+ currentChapterTicks += dummyChapterDuration;
+ }
- currentChapterTicks += dummyChapterDuration;
+ return chapters;
}
- return chapters;
+ return Array.Empty<ChapterInfo>();
}
}
}
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 3476e7000..0ddb2ad67 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -187,7 +188,7 @@ namespace MediaBrowser.Providers.Music
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = albumArtist,
- Type = "AlbumArtist"
+ Type = PersonKind.AlbumArtist
});
}
@@ -196,7 +197,7 @@ namespace MediaBrowser.Providers.Music
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = artist,
- Type = "Artist"
+ Type = PersonKind.Artist
});
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index dfaba6423..3fd4ae1fc 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -13,6 +13,7 @@ using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -424,7 +425,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var person = new PersonInfo
{
Name = result.Director,
- Type = PersonType.Director
+ Type = PersonKind.Director
};
itemResult.AddPerson(person);
@@ -435,7 +436,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var person = new PersonInfo
{
Name = result.Writer,
- Type = PersonType.Writer
+ Type = PersonKind.Writer
};
itemResult.AddPerson(person);
@@ -454,7 +455,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var person = new PersonInfo
{
Name = actor,
- Type = PersonType.Actor
+ Type = PersonKind.Actor
};
itemResult.AddPerson(person);
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index 0fb9d30a6..ae244da19 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
+ return new ImageType[]
{
ImageType.Thumb
};
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index fc7202366..2f62e117e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
@@ -258,7 +259,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
Name = actor.Name.Trim(),
Role = actor.Character,
- Type = PersonType.Actor,
+ Type = PersonKind.Actor,
SortOrder = actor.Order
};
@@ -278,20 +279,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieResult.Credits?.Crew is not null)
{
- var keepTypes = new[]
- {
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
- };
-
foreach (var person in movieResult.Credits.Crew)
{
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase) &&
- !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (!TmdbUtils.WantedCrewKinds.Contains(type)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 66decde84..f18575aa9 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
@@ -168,7 +169,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
Name = actor.Name.Trim(),
Role = actor.Character,
- Type = PersonType.Actor,
+ Type = PersonKind.Actor,
SortOrder = actor.Order
});
}
@@ -182,7 +183,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
Name = guest.Name.Trim(),
Role = guest.Character,
- Type = PersonType.GuestStar,
+ Type = PersonKind.GuestStar,
SortOrder = guest.Order
});
}
@@ -196,7 +197,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ if (!TmdbUtils.WantedCrewKinds.Contains(type)
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 3cb72b89b..10efb68b9 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
@@ -88,7 +89,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
Name = cast[i].Name.Trim(),
Role = cast[i].Character,
- Type = PersonType.Actor,
+ Type = PersonKind.Actor,
SortOrder = cast[i].Order
});
}
@@ -101,7 +102,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ if (!TmdbUtils.WantedCrewKinds.Contains(type)
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 09d1a739d..8dc2d6938 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
@@ -352,7 +353,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
Name = actor.Name.Trim(),
Role = actor.Character,
- Type = PersonType.Actor,
+ Type = PersonKind.Actor,
SortOrder = actor.Order,
ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
};
@@ -380,8 +381,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
- && !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (!TmdbUtils.WantedCrewKinds.Contains(type)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index b326d22c8..516eee758 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
using TMDbLib.Objects.General;
@@ -40,6 +41,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
};
/// <summary>
+ /// The crew kinds to keep.
+ /// </summary>
+ public static readonly PersonKind[] WantedCrewKinds =
+ {
+ PersonKind.Director,
+ PersonKind.Writer,
+ PersonKind.Producer
+ };
+
+ /// <summary>
/// Cleans the name according to TMDb requirements.
/// </summary>
/// <param name="name">The name of the entity.</param>
@@ -55,26 +66,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="crew">Crew member to map against the Jellyfin person types.</param>
/// <returns>The Jellyfin person type.</returns>
- public static string MapCrewToPersonType(Crew crew)
+ public static PersonKind MapCrewToPersonType(Crew crew)
{
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase))
{
- return PersonType.Director;
+ return PersonKind.Director;
}
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase))
{
- return PersonType.Producer;
+ return PersonKind.Producer;
}
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase))
{
- return PersonType.Writer;
+ return PersonKind.Writer;
}
- return string.Empty;
+ return PersonKind.Unknown;
}
/// <summary>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 8bd30447a..111d0c5cb 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Providers;
@@ -530,7 +531,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "director":
{
var val = reader.ReadElementContentAsString();
- foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Director }))
+ foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@@ -552,7 +553,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var parts = val.Split('/').Select(i => i.Trim())
.Where(i => !string.IsNullOrEmpty(i));
- foreach (var p in parts.Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer }))
+ foreach (var p in parts.Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@@ -569,7 +570,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "writer":
{
var val = reader.ReadElementContentAsString();
- foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer }))
+ foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@@ -1206,7 +1207,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
private PersonInfo GetPersonFromXmlNode(XmlReader reader)
{
var name = string.Empty;
- var type = PersonType.Actor; // If type is not specified assume actor
+ var type = PersonKind.Actor; // If type is not specified assume actor
var role = string.Empty;
int? sortOrder = null;
string? imageUrl = null;
@@ -1240,21 +1241,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "type":
{
var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
+ if (!Enum.TryParse(val, true, out type))
{
- type = val switch
- {
- PersonType.Composer => PersonType.Composer,
- PersonType.Conductor => PersonType.Conductor,
- PersonType.Director => PersonType.Director,
- PersonType.Lyricist => PersonType.Lyricist,
- PersonType.Producer => PersonType.Producer,
- PersonType.Writer => PersonType.Writer,
- PersonType.GuestStar => PersonType.GuestStar,
- // unknown type --> actor
- _ => PersonType.Actor
- };
+ type = PersonKind.Actor;
}
break;
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 130d0bfe4..4f8f869ac 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@@ -485,7 +486,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
var people = libraryManager.GetPeople(item);
var directors = people
- .Where(i => IsPersonType(i, PersonType.Director))
+ .Where(i => i.IsType(PersonKind.Director))
.Select(i => i.Name)
.ToList();
@@ -495,7 +496,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
var writers = people
- .Where(i => IsPersonType(i, PersonType.Writer))
+ .Where(i => i.IsType(PersonKind.Writer))
.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -913,7 +914,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
foreach (var person in people)
{
- if (IsPersonType(person, PersonType.Director) || IsPersonType(person, PersonType.Writer))
+ if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
{
continue;
}
@@ -930,9 +931,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("role", person.Role);
}
- if (!string.IsNullOrWhiteSpace(person.Type))
+ if (person.Type != PersonKind.Unknown)
{
- writer.WriteElementString("type", person.Type);
+ writer.WriteElementString("type", person.Type.ToString());
}
if (person.SortOrder.HasValue)
@@ -969,10 +970,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
return libraryManager.GetPathAfterNetworkSubstitution(image.Path);
}
- private bool IsPersonType(PersonInfo person, string type)
- => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
- || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
-
private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseNfoSaver> logger)
{
var settings = new XmlReaderSettings()
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index 21e7e2335..82e1dc860 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -62,7 +62,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
yield return Path.ChangeExtension(item.Path, ".nfo");
- if (!item.IsInMixedFolder)
+ // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
+ if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
{
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
}
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 95b08eb05..36435194f 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 18fb7bebe..c8943a399 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index e0555cd22..7b9a3de4e 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index ad5a0890b..32695e3f1 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 2d8be1835..8ffbeafad 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 6cb98b2b8..198dc63ef 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.MediaEncoding.Probing;
@@ -314,15 +315,15 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
Assert.Equal(22, res.People.Length);
Assert.Equal("Krysta Youngs", res.People[0].Name);
- Assert.Equal(PersonType.Composer, res.People[0].Type);
+ Assert.Equal(PersonKind.Composer, res.People[0].Type);
Assert.Equal("Julia Ross", res.People[1].Name);
- Assert.Equal(PersonType.Composer, res.People[1].Type);
+ Assert.Equal(PersonKind.Composer, res.People[1].Type);
Assert.Equal("Yiwoomin", res.People[2].Name);
- Assert.Equal(PersonType.Composer, res.People[2].Type);
+ Assert.Equal(PersonKind.Composer, res.People[2].Type);
Assert.Equal("Ji-hyo Park", res.People[3].Name);
- Assert.Equal(PersonType.Lyricist, res.People[3].Type);
+ Assert.Equal(PersonKind.Lyricist, res.People[3].Type);
Assert.Equal("Yiwoomin", res.People[4].Name);
- Assert.Equal(PersonType.Actor, res.People[4].Type);
+ Assert.Equal(PersonKind.Actor, res.People[4].Type);
Assert.Equal("Electric Piano", res.People[4].Role);
Assert.Equal(4, res.Genres.Length);
Assert.Contains("Electronic", res.Genres);
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
index 368c3592e..97b52f749 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -236,7 +236,7 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
- public void TestFalsePositive()
+ public void TestMissingParttype()
{
var files = new[]
{
@@ -248,9 +248,8 @@ namespace Jellyfin.Naming.Tests.Video
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
- Assert.Single(result);
-
- TestStackInfo(result[0], "300", 3);
+ // There should be no stack, because all files should be treated as separate movies
+ Assert.Empty(result);
}
[Fact]
@@ -297,11 +296,11 @@ namespace Jellyfin.Naming.Tests.Video
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
- Assert.Equal(3, result.Count);
+ // Only 'Bad Boys (2006)' and '300 (2006)' should be in the stack
+ Assert.Equal(2, result.Count);
TestStackInfo(result[0], "300 (2006)", 4);
- TestStackInfo(result[1], "300", 3);
- TestStackInfo(result[2], "Bad Boys (2006)", 4);
+ TestStackInfo(result[1], "Bad Boys (2006)", 4);
}
[Fact]
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index cc9cfdd7d..0316377d4 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -332,7 +332,9 @@ namespace Jellyfin.Naming.Tests.Video
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ // The result should contain two individual movies
+ // Version grouping should not work here, because the files are not in a directory with the name 'Four Sisters and a Wedding'
+ Assert.Equal(2, result.Count);
}
[Fact]
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
index 5ca59f0ed..400e30bd6 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
@@ -368,8 +368,8 @@ namespace Jellyfin.Providers.Tests.Manager
[Theory]
[InlineData(nameof(ICustomMetadataProvider), true)]
[InlineData(nameof(IRemoteMetadataProvider), true)]
- [InlineData(nameof(ILocalMetadataProvider), false)]
- public void GetMetadataProviders_CanRefreshMetadataOwned_WhenNotLocal(string providerType, bool expected)
+ [InlineData(nameof(ILocalMetadataProvider), true)]
+ public void GetMetadataProviders_CanRefreshMetadataOwned(string providerType, bool expected)
{
GetMetadataProviders_CanRefreshMetadata_Tester(providerType, expected, ownedItem: true);
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
index 599599071..562711337 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
@@ -80,6 +80,35 @@ public class FindExtrasTests
}
[Fact]
+ public void FindExtras_SeparateMovieFolder_CleanExtraNames()
+ {
+ var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
+ var paths = new List<string>
+ {
+ "/movies/Up/Up.mkv",
+ "/movies/Up/Recording the audio[Bluray]-behindthescenes.mkv",
+ "/movies/Up/Interview with the dog-interview.mkv",
+ "/movies/Up/shorts/Balloons[1080p].mkv"
+ };
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ IsDirectory = false
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
+
+ Assert.Equal(3, extras.Count);
+ Assert.Equal(ExtraType.BehindTheScenes, extras[0].ExtraType);
+ Assert.Equal("Recording the audio", extras[0].Name);
+ Assert.Equal(ExtraType.Interview, extras[1].ExtraType);
+ Assert.Equal("Interview with the dog", extras[1].Name);
+ Assert.Equal(ExtraType.Short, extras[2].ExtraType);
+ Assert.Equal("Balloons", extras[2].Name);
+ }
+
+ [Fact]
public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsCorrectExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index ab3682ccf..7fabe9904 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(53, ratings.Count);
+ Assert.Equal(54, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
@@ -100,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(18, ratings.Count);
+ Assert.Equal(19, ratings.Count);
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index 4f4ae5afb..f63bc0e1b 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -79,18 +80,18 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal("1276153", item.ProviderIds[MetadataProvider.Tmdb.ToString()]);
// Credits
- var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray();
+ var writers = result.People.Where(x => x.Type == PersonKind.Writer).ToArray();
Assert.Equal(2, writers.Length);
Assert.Contains("Bryan Fuller", writers.Select(x => x.Name));
Assert.Contains("Michael Green", writers.Select(x => x.Name));
// Direcotrs
- var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray();
+ var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray();
Assert.Single(directors);
Assert.Contains("David Slade", directors.Select(x => x.Name));
// Actors
- var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray();
+ var actors = result.People.Where(x => x.Type == PersonKind.Actor).ToArray();
Assert.Equal(11, actors.Length);
// Only test one actor
var shadow = actors.FirstOrDefault(x => x.Role.Equals("Shadow Moon", StringComparison.Ordinal));
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 988abce81..f56f58c6f 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -117,18 +118,18 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(20, result.People.Count);
- var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray();
+ var writers = result.People.Where(x => x.Type == PersonKind.Writer).ToArray();
Assert.Equal(3, writers.Length);
var writerNames = writers.Select(x => x.Name);
Assert.Contains("Jerry Siegel", writerNames);
Assert.Contains("Joe Shuster", writerNames);
Assert.Contains("Test", writerNames);
- var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray();
+ var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray();
Assert.Single(directors);
Assert.Equal("Zack Snyder", directors[0].Name);
- var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray();
+ var actors = result.People.Where(x => x.Type == PersonKind.Actor).ToArray();
Assert.Equal(15, actors.Length);
// Only test one actor
@@ -138,7 +139,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(5, aquaman!.SortOrder);
Assert.Equal("https://m.media-amazon.com/images/M/MV5BMTI5MTU5NjM1MV5BMl5BanBnXkFtZTcwODc4MDk0Mw@@._V1_SX1024_SY1024_.jpg", aquaman!.ImageUrl);
- var lyricist = result.People.FirstOrDefault(x => x.Type == PersonType.Lyricist);
+ var lyricist = result.People.FirstOrDefault(x => x.Type == PersonKind.Lyricist);
Assert.NotNull(lyricist);
Assert.Equal("Test Lyricist", lyricist!.Name);
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
index 31110dbd7..e69ca996c 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -60,7 +61,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(10, result.People.Count);
- Assert.True(result.People.All(x => x.Type == PersonType.Actor));
+ Assert.True(result.People.All(x => x.Type == PersonKind.Actor));
// Only test one actor
var nini = result.People.FirstOrDefault(x => x.Role.Equals("Nini", StringComparison.Ordinal));
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
index bdedae205..542324e78 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -67,7 +68,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(6, result.People.Count);
- Assert.True(result.People.All(x => x.Type == PersonType.Actor));
+ Assert.True(result.People.All(x => x.Type == PersonKind.Actor));
// Only test one actor
var sweeney = result.People.FirstOrDefault(x => x.Role.Equals("Mad Sweeney", StringComparison.Ordinal));