aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml8
-rw-r--r--.github/workflows/ci-compat.yml8
-rw-r--r--.github/workflows/ci-openapi.yml12
-rw-r--r--.github/workflows/ci-tests.yml4
-rw-r--r--.github/workflows/commands.yml4
-rw-r--r--.github/workflows/issue-template-check.yml2
-rw-r--r--.github/workflows/release-bump-version.yaml4
-rw-r--r--CONTRIBUTORS.md3
-rw-r--r--Directory.Packages.props65
-rw-r--r--Emby.Naming/Common/NamingOptions.cs28
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParser.cs8
-rw-r--r--Emby.Naming/Video/ExtraRuleResolver.cs78
-rw-r--r--Emby.Photos/PhotoProvider.cs2
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs169
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs37
-rw-r--r--Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs29
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs30
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json88
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json56
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json92
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nn.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/uz.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json16
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs9
-rw-r--r--Emby.Server.Implementations/Localization/iso6392.txt23
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs119
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs36
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs5
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs11
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs60
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs11
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs2
-rw-r--r--Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs60
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs119
-rw-r--r--Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs10
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj1
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationService.cs8
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs10
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs18
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs74
-rw-r--r--Jellyfin.Server/Migrations/Stages/CodeMigration.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs14
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs8
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs12
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs12
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs2
-rw-r--r--MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs29
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs6
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs35
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs106
-rw-r--r--MediaBrowser.Model/Dto/ItemCounts.cs9
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs54
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs1
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs8
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs27
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs19
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs2
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs22
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs2
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayProvider.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.Designer.cs1709
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs22
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs11
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs33
-rw-r--r--src/Jellyfin.LiveTv/IO/EncodedRecorder.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs11
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/CoreResolutionIgnoreRuleTest.cs129
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs2
104 files changed, 3173 insertions, 588 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 7a8163c33..5efa53e31 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "9.0.6",
+ "version": "9.0.8",
"commands": [
"dotnet-ef"
]
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index cd5b11603..c7f7bea35 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+ uses: github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+ uses: github/codeql-action/autobuild@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+ uses: github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index 7dfcd9477..00670410c 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -11,7 +11,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -40,7 +40,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -85,13 +85,13 @@ jobs:
steps:
- name: Download abi-head
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: abi-head
path: abi-head
- name: Download abi-base
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: abi-base
path: abi-base
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 966d86ed2..809aecf7e 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -16,7 +16,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -41,7 +41,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -80,12 +80,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: openapi-base
path: openapi-base
@@ -158,7 +158,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: openapi-head
path: openapi-head
@@ -220,7 +220,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: openapi-head
path: openapi-head
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index cbda4b80d..d840e52b1 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@4c0f60daf67483745c34efdeadd4c4e78a19991e # v5.4.8
+ uses: danielpalme/ReportGenerator-GitHub-Action@c4c5175a441c6603ec614f5084386dabe0e2295b # v5.4.12
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 374554ac0..261c179af 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index 588f0a50e..707c77279 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -10,7 +10,7 @@ jobs:
issues: write
steps:
- name: pull in script
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml
index 0da9c44f8..ec91744f3 100644
--- a/.github/workflows/release-bump-version.yaml
+++ b/.github/workflows/release-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 06c72c616..d2895cabe 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -61,6 +61,7 @@
- [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga)
+ - [jkhsjdhjs](https://github.com/jkhsjdhjs)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -199,6 +200,8 @@
- [allesmi](https://github.com/allesmi)
- [ThunderClapLP](https://github.com/ThunderClapLP)
- [Shoham Peller](https://github.com/spellr)
+ - [theshoeshiner](https://github.com/theshoeshiner)
+ - [TokerX](https://github.com/TokerX)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 61093ad99..f075db3a6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,12 +9,12 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
- <PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
+ <PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
- <PackageVersion Include="Diacritics" Version="3.3.29" />
+ <PackageVersion Include="Diacritics" Version="4.0.17" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
@@ -26,42 +26,43 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.6" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.6" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.6" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.6" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.8" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
- <PackageVersion Include="NEbml" Version="0.12.0" />
+ <PackageVersion Include="NEbml" Version="1.0.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
- <PackageVersion Include="Polly" Version="8.6.1" />
+ <PackageVersion Include="Polly" Version="8.6.2" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
+ <PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
@@ -75,18 +76,18 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="3.0.3" />
+ <PackageVersion Include="Svg.Skia" Version="3.0.5" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
- <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.6" />
- <PackageVersion Include="System.Text.Json" Version="9.0.6" />
- <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.6" />
+ <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.8" />
+ <PackageVersion Include="System.Text.Json" Version="9.0.8" />
+ <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.8" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.26.0" />
+ <PackageVersion Include="z440.atl.core" Version="7.3.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
- <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
+ <PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 192235bae..1c518f0cc 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -573,6 +573,18 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Filename,
+ "sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.Filename,
+ "theme",
+ MediaType.Audio),
+
+ new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
"-trailer",
@@ -593,13 +605,7 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
- " trailer",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.Sample,
- ExtraRuleType.Filename,
- "sample",
+ "- trailer",
MediaType.Video),
new ExtraRule(
@@ -623,16 +629,10 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
- " sample",
+ "- sample",
MediaType.Video),
new ExtraRule(
- ExtraType.ThemeSong,
- ExtraRuleType.Filename,
- "theme",
- MediaType.Audio),
-
- new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
"-scene",
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 7a01b02f3..3461b3c0d 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -97,14 +97,18 @@ namespace Emby.Naming.ExternalFiles
if (culture is not null && pathInfo.Language is null)
{
- pathInfo.Language = culture.ThreeLetterISOLanguageName;
+ pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
+ ? culture.Name
+ : culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (culture is not null && pathInfo.Language == "hin")
{
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
pathInfo.IsHearingImpaired = true;
- pathInfo.Language = culture.ThreeLetterISOLanguageName;
+ pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
+ ? culture.Name
+ : culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index 528906589..2e0caa612 100644
--- a/Emby.Naming/Video/ExtraRuleResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -22,67 +22,45 @@ namespace Emby.Naming.Video
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
{
- var result = new ExtraResult();
+ ExtraResult result = new ExtraResult();
- for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
+ bool isAudioFile = AudioFileParser.IsAudioFile(path, namingOptions);
+ bool isVideoFile = VideoResolver.IsVideoFile(path, namingOptions);
+
+ ReadOnlySpan<char> pathSpan = path.AsSpan();
+ ReadOnlySpan<char> fileName = Path.GetFileName(pathSpan);
+ ReadOnlySpan<char> fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathSpan);
+ // Trim the digits from the end of the filename so we can recognize things like -trailer2
+ ReadOnlySpan<char> trimmedFileNameWithoutExtension = fileNameWithoutExtension.TrimEnd(_digits);
+ ReadOnlySpan<char> directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
+ string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
+
+ foreach (ExtraRule rule in namingOptions.VideoExtraRules)
{
- var rule = namingOptions.VideoExtraRules[i];
- if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
- || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
+ if ((rule.MediaType == MediaType.Audio && !isAudioFile)
+ || (rule.MediaType == MediaType.Video && !isVideoFile))
{
continue;
}
- var pathSpan = path.AsSpan();
- if (rule.RuleType == ExtraRuleType.Filename)
+ bool isMatch = rule.RuleType switch
{
- var filename = Path.GetFileNameWithoutExtension(pathSpan);
+ ExtraRuleType.Filename => fileNameWithoutExtension.Equals(rule.Token, StringComparison.OrdinalIgnoreCase),
+ ExtraRuleType.Suffix => trimmedFileNameWithoutExtension.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase),
+ ExtraRuleType.Regex => Regex.IsMatch(fileName, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ ExtraRuleType.DirectoryName => directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase),
+ _ => false,
+ };
- if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Suffix)
+ if (!isMatch)
{
- // Trim the digits from the end of the filename so we can recognize things like -trailer2
- var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
-
- if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Regex)
- {
- var filename = Path.GetFileName(path.AsSpan());
-
- var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
-
- if (isMatch)
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.DirectoryName)
- {
- var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
- string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
- if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
- && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
+ continue;
}
- if (result.ExtraType is not null)
- {
- return result;
- }
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ return result;
}
return result;
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index 4aca412ba..e24c067d6 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
if (item.IsFileProtocol)
{
var file = directoryService.GetFile(item.Path);
- return file is not null && file.LastWriteTimeUtc != item.DateModified;
+ return file is not null && item.HasChanged(file.LastWriteTimeUtc);
}
return false;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index cf886ae82..0db1606ea 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -37,6 +38,77 @@ namespace Emby.Server.Implementations.Dto
{
public class DtoService : IDtoService
{
+ private static readonly FrozenDictionary<BaseItemKind, BaseItemKind[]> _relatedItemKinds = new Dictionary<BaseItemKind, BaseItemKind[]>
+ {
+ {
+ BaseItemKind.Genre, [
+ BaseItemKind.Audio,
+ BaseItemKind.Episode,
+ BaseItemKind.Movie,
+ BaseItemKind.LiveTvProgram,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.Series,
+ BaseItemKind.Trailer
+ ]
+ },
+ {
+ BaseItemKind.MusicArtist, [
+ BaseItemKind.Audio,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicVideo
+ ]
+ },
+ {
+ BaseItemKind.MusicGenre, [
+ BaseItemKind.Audio,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.MusicVideo
+ ]
+ },
+ {
+ BaseItemKind.Person, [
+ BaseItemKind.Audio,
+ BaseItemKind.Episode,
+ BaseItemKind.Movie,
+ BaseItemKind.LiveTvProgram,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.Series,
+ BaseItemKind.Trailer
+ ]
+ },
+ {
+ BaseItemKind.Studio, [
+ BaseItemKind.Audio,
+ BaseItemKind.Episode,
+ BaseItemKind.Movie,
+ BaseItemKind.LiveTvProgram,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.Series,
+ BaseItemKind.Trailer
+ ]
+ },
+ {
+ BaseItemKind.Year, [
+ BaseItemKind.Audio,
+ BaseItemKind.Episode,
+ BaseItemKind.Movie,
+ BaseItemKind.LiveTvProgram,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.Series,
+ BaseItemKind.Trailer
+ ]
+ }
+ }.ToFrozenDictionary();
+
private readonly ILogger<DtoService> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataRepository;
@@ -102,21 +174,9 @@ namespace Emby.Server.Implementations.Dto
(programTuples ??= []).Add((item, dto));
}
- if (item is IItemByName byName)
+ if (options.ContainsField(ItemFields.ItemCounts))
{
- if (options.ContainsField(ItemFields.ItemCounts))
- {
- var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user)
- {
- Recursive = true,
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- });
-
- SetItemByNameInfo(item, dto, libraryItems);
- }
+ SetItemByNameInfo(dto, user);
}
returnItems[index] = dto;
@@ -147,34 +207,14 @@ namespace Emby.Server.Implementations.Dto
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
}
- if (item is IItemByName itemByName
- && options.ContainsField(ItemFields.ItemCounts))
+ if (options.ContainsField(ItemFields.ItemCounts))
{
- SetItemByNameInfo(
- item,
- dto,
- GetTaggedItems(
- itemByName,
- user,
- new DtoOptions(false)
- {
- EnableImages = false
- }));
+ SetItemByNameInfo(dto, user);
}
return dto;
}
- private static IReadOnlyList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
- {
- return byName.GetTaggedItems(
- new InternalItemsQuery(user)
- {
- Recursive = true,
- DtoOptions = options
- });
- }
-
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{
var dto = new BaseItemDto
@@ -315,11 +355,15 @@ namespace Emby.Server.Implementations.Dto
}
/// <inheritdoc />
+ /// TODO refactor this to use the new SetItemByNameInfo.
+ /// Some callers already have the counts extracted so no reason to retrieve them again.
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null)
{
var dto = GetBaseItemDtoInternal(item, options, user);
- if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts))
+ if (options.ContainsField(ItemFields.ItemCounts)
+ && taggedItems is not null
+ && taggedItems.Count != 0)
{
SetItemByNameInfo(item, dto, taggedItems);
}
@@ -327,6 +371,57 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
+ private void SetItemByNameInfo(BaseItemDto dto, User? user)
+ {
+ if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
+ {
+ return;
+ }
+
+ var query = new InternalItemsQuery(user)
+ {
+ Recursive = true,
+ DtoOptions = new DtoOptions(false) { EnableImages = false },
+ IncludeItemTypes = relatedItemKinds
+ };
+
+ switch (dto.Type)
+ {
+ case BaseItemKind.Genre:
+ case BaseItemKind.MusicGenre:
+ query.GenreIds = [dto.Id];
+ break;
+ case BaseItemKind.MusicArtist:
+ query.ArtistIds = [dto.Id];
+ break;
+ case BaseItemKind.Person:
+ query.PersonIds = [dto.Id];
+ break;
+ case BaseItemKind.Studio:
+ query.StudioIds = [dto.Id];
+ break;
+ case BaseItemKind.Year
+ when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
+ query.Years = [year];
+ break;
+ default:
+ return;
+ }
+
+ var counts = _libraryManager.GetItemCounts(query);
+
+ dto.AlbumCount = counts.AlbumCount;
+ dto.ArtistCount = counts.ArtistCount;
+ dto.EpisodeCount = counts.EpisodeCount;
+ dto.MovieCount = counts.MovieCount;
+ dto.MusicVideoCount = counts.MusicVideoCount;
+ dto.ProgramCount = counts.ProgramCount;
+ dto.SeriesCount = counts.SeriesCount;
+ dto.SongCount = counts.SongCount;
+ dto.TrailerCount = counts.TrailerCount;
+ dto.ChildCount = counts.TotalItemCount();
+ }
+
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
{
if (item is MusicArtist)
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index f29a0b3ad..f9538fbad 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -38,7 +38,8 @@ namespace Emby.Server.Implementations.Library
}
// Don't ignore top level folders
- if (fileInfo.IsDirectory && parent is AggregateFolder)
+ if (fileInfo.IsDirectory
+ && (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
{
return false;
}
@@ -48,35 +49,21 @@ namespace Emby.Server.Implementations.Library
return true;
}
- var filename = fileInfo.Name;
-
- if (fileInfo.IsDirectory)
+ if (parent is null)
{
- if (parent is not null)
- {
- // Ignore extras for unsupported types
- if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
- && parent is not AggregateFolder
- && parent is not UserRootFolder)
- {
- return true;
- }
- }
+ return false;
}
- else
+
+ if (fileInfo.IsDirectory)
{
- if (parent is not null)
- {
- // Don't resolve theme songs
- if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
- && AudioFileParser.IsAudioFile(filename, _namingOptions))
- {
- return true;
- }
- }
+ // Ignore extras for unsupported types
+ return _namingOptions.AllExtrasTypesFolderNames.ContainsKey(fileInfo.Name)
+ && parent is not UserRootFolder;
}
- return false;
+ // Don't resolve theme songs
+ return Path.GetFileNameWithoutExtension(fileInfo.Name.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
+ && AudioFileParser.IsAudioFile(fileInfo.Name, _namingOptions);
}
}
}
diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
index b0ed1de8d..401ca73b8 100644
--- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
@@ -42,6 +42,19 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <returns>True if the file should be ignored.</returns>
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{
+ if (fileInfo.IsDirectory)
+ {
+ var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName));
+ if (dirIgnoreFile is null)
+ {
+ return false;
+ }
+
+ // ignore the directory only if the .ignore file is empty
+ // evaluate individual files otherwise
+ return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
+ }
+
var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
if (string.IsNullOrEmpty(parentDirPath))
{
@@ -55,13 +68,9 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return false;
}
- string ignoreFileString;
- using (var reader = ignoreFile.OpenText())
- {
- ignoreFileString = reader.ReadToEnd();
- }
+ string ignoreFileString = GetFileContent(ignoreFile);
- if (string.IsNullOrEmpty(ignoreFileString))
+ if (string.IsNullOrWhiteSpace(ignoreFileString))
{
// Ignore directory if we just have the file
return true;
@@ -74,4 +83,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return ignore.IsIgnored(fileInfo.FullName);
}
+
+ private static string GetFileContent(FileInfo dirIgnoreFile)
+ {
+ using (var reader = dirIgnoreFile.OpenText())
+ {
+ return reader.ReadToEnd();
+ }
+ }
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 6ffe67776..58a971f62 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1389,6 +1389,25 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetCount(query);
}
+ public ItemCounts GetItemCounts(InternalItemsQuery query)
+ {
+ if (query.Recursive && !query.ParentId.IsEmpty())
+ {
+ var parent = GetItemById(query.ParentId);
+ if (parent is not null)
+ {
+ SetTopParentIdsOrAncestors(query, [parent]);
+ }
+ }
+
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ return _itemRepository.GetItemCounts(query);
+ }
+
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
{
SetTopParentIdsOrAncestors(query, parents);
@@ -1954,7 +1973,7 @@ namespace Emby.Server.Implementations.Library
try
{
- return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
+ return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeconds > 1;
}
catch (Exception ex)
{
@@ -2025,7 +2044,8 @@ namespace Emby.Server.Implementations.Library
try
{
- image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
+ var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
+ image.BlurHash = blurhash;
}
catch (Exception ex)
{
@@ -2035,7 +2055,8 @@ namespace Emby.Server.Implementations.Library
try
{
- image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
+ var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
+ image.DateModified = modifiedDate;
}
catch (Exception ex)
{
@@ -2044,6 +2065,7 @@ namespace Emby.Server.Implementations.Library
}
_itemRepository.SaveImages(item);
+
RegisterItem(item);
}
@@ -3047,6 +3069,8 @@ namespace Emby.Server.Implementations.Library
}
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+ personEntity.DateLastSaved = DateTime.UtcNow;
+
CreateItems([personEntity], null, CancellationToken.None);
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index ab30971e2..1e3b8ea76 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -379,7 +379,7 @@ namespace Emby.Server.Implementations.Library
var culture = _localizationManager.FindLanguageInfo(language);
if (culture is not null)
{
- return culture.ThreeLetterISOLanguageNames;
+ return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLetterISOLanguageNames;
}
return [language];
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index d5da04fb9..dec491d08 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -135,5 +135,7 @@
"TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
- "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay"
+ "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
+ "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
+ "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 72f575753..fd3666ef1 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -136,5 +136,7 @@
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
"TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
- "TaskExtractMediaSegments": "Сканиране за сегменти"
+ "TaskExtractMediaSegments": "Сканиране за сегменти",
+ "CleanupUserDataTask": "Задача за почистване на потребителски данни",
+ "CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
}
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 268a141ff..fad3715f2 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -6,29 +6,29 @@
"Channels": "চ্যানেলসমূহ",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "পুস্তকসমূহ",
- "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
+ "AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন",
"Artists": "শিল্পীগণ",
"Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামসমূহ",
- "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
+ "HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
- "Genres": "শৈলীধারাসমূহ",
+ "Genres": "জনরা",
"Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
- "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
+ "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}",
"VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
- "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
- "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
+ "UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}",
+ "UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}",
"UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
"UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
- "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন",
- "UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে",
+ "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে",
+ "UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে",
"UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না",
"UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
"UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
@@ -36,8 +36,8 @@
"User": "ব্যবহারকারী",
"TvShows": "টিভি শোগুলো",
"System": "সিস্টেম",
- "Sync": "সমলয় স্থাপন",
- "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
+ "Sync": "সমন্বয় করুন",
+ "SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
"Songs": "সঙ্গীতসমূহ",
"Shows": "টিভি পর্ব",
@@ -46,18 +46,18 @@
"ScheduledTaskFailedWithName": "{0} ব্যর্থ",
"ProviderValue": "প্রদানকারী: {0}",
"PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
- "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
- "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
+ "PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে",
+ "PluginInstalledWithName": "{0} ইন্সটল হয়েছে",
"Plugin": "প্লাগিন",
"Playlists": "প্লে লিস্ট সমূহ",
- "Photos": "চিত্রসমূহ",
- "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
- "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
+ "Photos": "ছবিসমূহ",
+ "NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে",
+ "NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে",
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
"NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
- "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক",
- "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে",
- "NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে",
+ "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে",
+ "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে",
+ "NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে",
"NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
"NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
"NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
@@ -76,8 +76,8 @@
"Movies": "চলচ্চিত্রসমূহ",
"MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
- "HeaderRecordingGroups": "রেকর্ডিং দল",
- "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
+ "HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো",
+ "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে",
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
"Latest": "সর্বশেষ",
@@ -85,51 +85,57 @@
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
- "Inherit": "থেকে পাওয়া",
+ "Inherit": "মূল থেকে গ্রহণ করুন",
"HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
"HeaderFavoriteShows": "প্রিয় শোগুলো",
- "TasksLibraryCategory": "গ্রন্থাগার",
+ "TasksLibraryCategory": "লাইব্রেরি",
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
"TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
- "TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।",
- "TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন",
- "TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।",
+ "TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।",
+ "TaskRefreshChapterImages": "চ্যাপ্টার ইমেজ বের করুন",
+ "TaskCleanCacheDescription": "সিস্টেমের অপ্রয়োজনীয় ক্যাশ ফাইলগুলো মুছে ফেলবে।",
"TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
"TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
- "TasksApplicationCategory": "আবেদন",
+ "TasksApplicationCategory": "অ্যাপ্লিকেশন",
"TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
"TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
"TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
"TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
- "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।",
+ "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।",
"TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
"TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
- "TaskUpdatePlugins": "প্লাগইন আপডেট করুন",
- "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।",
- "TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
- "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
- "TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
- "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
+ "TaskUpdatePlugins": "আপডেট প্লাগইন",
+ "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।",
+ "TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ",
+ "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।",
+ "TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি",
+ "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।",
"Undefined": "অসঙ্গায়িত",
"Forced": "জোরকরে",
- "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
- "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
+ "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।",
+ "TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন",
"Default": "ডিফল্ট",
- "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
+ "HearingImpaired": "শ্রবণ প্রতিবন্ধী",
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
"External": "বাহ্যিক",
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
- "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
+ "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
"TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
- "TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন",
- "TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
+ "TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
+ "TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
- "TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
- "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন"
+ "TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
+ "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
+ "TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।",
+ "TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।",
+ "CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।",
+ "TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন",
+ "TaskAudioNormalization": "অডিও নর্মলাইজেশন",
+ "CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index d41aaa984..596df6348 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -43,7 +43,7 @@
"NameInstallFailed": "{0} instal·lació fallida",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconeguda",
- "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
+ "NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.",
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
@@ -64,7 +64,7 @@
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
- "PluginUninstalledWithName": "S'ha instalat {0}",
+ "PluginUninstalledWithName": "S'ha instal·lat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
@@ -93,50 +93,50 @@
"ValueSpecialEpisodeName": "Especial - {0}",
"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",
+ "TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
"TaskRefreshChannels": "Actualitza els canals",
- "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
- "TaskCleanTranscode": "Neteja les transcodificacions",
+ "TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.",
+ "TaskCleanTranscode": "Neteja de les transcodificacions",
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
- "TaskUpdatePlugins": "Actualitza els complements",
- "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la mediateca.",
- "TaskRefreshPeople": "Actualitza les persones",
+ "TaskUpdatePlugins": "Actualització dels complements",
+ "TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.",
+ "TaskRefreshPeople": "Actualització de les persones",
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
- "TaskCleanLogs": "Neteja els registres",
- "TaskRefreshLibraryDescription": "Escaneja la mediateca, a la cerca de fitxers nous i refresca les metadades.",
- "TaskRefreshLibrary": "Escaneja la mediateca",
- "TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
- "TaskRefreshChapterImages": "Extreu les imatges dels capítols",
- "TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
- "TaskCleanCache": "Elimina la memòria cau",
+ "TaskCleanLogs": "Neteja dels registres",
+ "TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
+ "TaskRefreshLibrary": "Escaneig de les mediateques",
+ "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
+ "TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
+ "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
+ "TaskCleanCache": "Eliminació de la memòria cau",
"TasksChannelsCategory": "Canals per internet",
"TasksApplicationCategory": "Aplicatiu",
"TasksLibraryCategory": "Mediateca",
"TasksMaintenanceCategory": "Manteniment",
- "TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
- "TaskCleanActivityLog": "Buida el registre d'activitat",
+ "TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
+ "TaskCleanActivityLog": "Buidatge del registre d'activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
"Default": "Per defecte",
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
- "TaskOptimizeDatabase": "Optimitza la base de dades",
- "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
- "TaskKeyframeExtractor": "Extractor de fotogrames clau",
+ "TaskOptimizeDatabase": "Optimització de la base de dades",
+ "TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
+ "TaskKeyframeExtractor": "Extracció de fotogrames clau",
"External": "Extern",
"HearingImpaired": "Discapacitat auditiva",
- "TaskRefreshTrickplayImages": "Genera imatges de previsualització",
- "TaskRefreshTrickplayImagesDescription": "Crea imatges de previsualització per vídeos en les mediateques habilitades.",
+ "TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
+ "TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
- "TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
+ "TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
"TaskAudioNormalization": "Estabilització de l'àudio",
- "TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització de l'àudio.",
- "TaskDownloadMissingLyricsDescription": "Baixa les lletres de les cançons",
- "TaskDownloadMissingLyrics": "Baixa les lletres que falten",
+ "TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
+ "TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
+ "TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin",
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
- "TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
- "TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la mediateca.",
+ "TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
+ "TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index d43d4097f..bbee38ba5 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -136,5 +136,7 @@
"TaskExtractMediaSegments": "Scan for mediesegmenter",
"TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
"TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
- "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment."
+ "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.",
+ "CleanupUserDataTask": "Brugerdata oprydningsopgave",
+ "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage."
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 8969b72c4..664da8249 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -90,7 +90,7 @@
"UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
"UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
"ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
- "ValueSpecialEpisodeName": "Extra - {0}",
+ "ValueSpecialEpisodeName": "Extra – {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
"TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index f3195f0ea..2ba2085da 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",
"TaskDownloadMissingLyricsDescription": "Κατεβάζει στίχους για τραγούδια",
"TaskExtractMediaSegments": "Σάρωση τμημάτων πολυμέσων",
- "TaskExtractMediaSegmentsDescription": "Εξάγει ή βρίσκει τμήματα πολυμέσων από επεκτάσεις που χρησιμοποιούν το MediaSegment."
+ "TaskExtractMediaSegmentsDescription": "Εξάγει ή βρίσκει τμήματα πολυμέσων από επεκτάσεις που χρησιμοποιούν το MediaSegment.",
+ "CleanupUserDataTaskDescription": "Καθαρίζει όλα τα δεδομένα χρήστη (κατάσταση παρακολούθησης, κατάσταση αγαπημένων κ.λπ.) από πολυμέσα που δεν υπάρχουν πλέον για τουλάχιστον 90 ημέρες.",
+ "CleanupUserDataTask": "Εργασία εκκαθάρισης δεδομένων χρήστη"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 5fb7e8ae1..20f38de62 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -136,5 +136,6 @@
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
- "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca."
+ "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
+ "CleanupUserDataTask": "Tarea de limpieza de los datos del usuario"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index 2534f37c1..34c68c33b 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -135,5 +135,7 @@
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
- "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay"
+ "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
+ "CleanupUserDataTask": "Tarea de limpieza de datos de usuario",
+ "CleanupUserDataTaskDescription": "Limpia todos los datos de usuario (estado de visualización, favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días."
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 55b309098..ff6f6d232 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -135,5 +135,6 @@
"TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
- "TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio."
+ "TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
+ "CleanupUserDataTask": "Tarefa de limpeza de datos do usuario"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 7293a0d38..90c921898 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -32,8 +32,8 @@
"LabelIpAddressValue": "Ip כתובת: {0}",
"LabelRunningTimeValue": "משך צפייה: {0}",
"Latest": "אחרון",
- "MessageApplicationUpdated": "שרת ג'ליפין עודכן",
- "MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}",
+ "MessageApplicationUpdated": "שרת Jellyfin עודכן",
+ "MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
"MixedContent": "תוכן מעורב",
@@ -43,7 +43,7 @@
"NameInstallFailed": "התקנת {0} נכשלה",
"NameSeasonNumber": "עונה {0}",
"NameSeasonUnknown": "עונה לא ידועה",
- "NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.",
+ "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
"NotificationOptionAudioPlayback": "ניגון שמע החל",
@@ -72,7 +72,7 @@
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות",
"Songs": "שירים",
- "StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.",
+ "StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
"Sync": "סנכרון",
@@ -100,14 +100,14 @@
"TasksLibraryCategory": "ספרייה",
"TasksMaintenanceCategory": "תחזוקה",
"TaskUpdatePlugins": "עדכן תוספים",
- "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
+ "TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.",
"TaskRefreshPeople": "רענן אנשים",
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
"TaskCleanLogs": "ניקוי תיקיית יומן",
- "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
+ "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.",
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט",
- "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
+ "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.",
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
"TaskRefreshChannels": "רענן ערוץ",
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index c7cb50e0c..81a996330 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -1,6 +1,6 @@
{
"Albums": "Albumok",
- "AppDeviceValues": "Program: {0}, eszköz: {1}",
+ "AppDeviceValues": "Program: {0}, Eszköz: {1}",
"Application": "Alkalmazás",
"Artists": "Előadók",
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 672c686fa..6f94df9d7 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -131,5 +131,8 @@
"TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
"TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.",
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
- "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar"
+ "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
+ "TaskExtractMediaSegments": "Skönnun efnishluta",
+ "CleanupUserDataTask": "Hreinsun notendagagna",
+ "CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga."
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index e05afbabe..421c4ee30 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
"TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
"TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
- "TaskExtractMediaSegments": "Scansiona Segmento Media"
+ "TaskExtractMediaSegments": "Scansiona Segmento Media",
+ "CleanupUserDataTask": "Task di pulizia dei dati utente",
+ "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 46fc49f5e..3918ab81c 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}",
"Channels": "Kanalai",
"ChapterNameValue": "Scena{0}",
- "Collections": "Kolekcijos",
+ "Collections": "Rinkiniai",
"DeviceOfflineWithName": "{0} buvo atjungtas",
"DeviceOnlineWithName": "{0} prisijungęs",
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
@@ -17,18 +17,18 @@
"Genres": "Žanrai",
"HeaderAlbumArtists": "Albumo atlikėjai",
"HeaderContinueWatching": "Žiūrėti toliau",
- "HeaderFavoriteAlbums": "Mėgstami Albumai",
- "HeaderFavoriteArtists": "Mėgstami Atlikėjai",
+ "HeaderFavoriteAlbums": "Mėgstami albumai",
+ "HeaderFavoriteArtists": "Mėgstami atlikėjai",
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
"HeaderFavoriteSongs": "Mėgstamos Dainos",
"HeaderLiveTV": "Tiesioginė TV",
- "HeaderNextUp": "Toliau eilėje",
+ "HeaderNextUp": "Toliau",
"HeaderRecordingGroups": "Įrašų grupės",
"HomeVideos": "Namų vaizdo įrašai",
"Inherit": "Paveldėti",
- "ItemAddedWithName": "{0} - buvo įkeltas į mediateką",
- "ItemRemovedWithName": "{0} - buvo pašalinta iš mediatekos",
+ "ItemAddedWithName": "{0} - buvo įkeltas į biblioteką",
+ "ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos",
"LabelIpAddressValue": "IP adresas: {0}",
"LabelRunningTimeValue": "Trukmė: {0}",
"Latest": "Naujausi",
@@ -36,7 +36,7 @@
"MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti",
"MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti",
- "MixedContent": "Mixed content",
+ "MixedContent": "Mišrus turinys",
"Movies": "Filmai",
"Music": "Muzika",
"MusicVideos": "Muzikiniai vaizdo įrašai",
@@ -53,21 +53,21 @@
"NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
"NotificationOptionPluginError": "Įskiepio klaida",
"NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
- "NotificationOptionPluginUninstalled": "Įskiepis pašalintas",
+ "NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
"NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas",
"NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas",
"NotificationOptionTaskFailed": "Suplanuotos užduoties klaida",
- "NotificationOptionUserLockedOut": "Vartotojas užblokuotas",
+ "NotificationOptionUserLockedOut": "Naudotojas užblokuotas",
"NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas",
"NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas",
"Photos": "Nuotraukos",
- "Playlists": "Grojaraštis",
- "Plugin": "Plugin",
+ "Playlists": "Grojaraščiai",
+ "Plugin": "Įskiepis",
"PluginInstalledWithName": "{0} buvo įdiegtas",
"PluginUninstalledWithName": "{0} buvo pašalintas",
"PluginUpdatedWithName": "{0} buvo atnaujintas",
- "ProviderValue": "Provider: {0}",
- "ScheduledTaskFailedWithName": "{0} klaida",
+ "ProviderValue": "Paslaugos tiekėjas: {0}",
+ "ScheduledTaskFailedWithName": "{0} nepavyko",
"ScheduledTaskStartedWithName": "{0} paleista",
"ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti",
"Shows": "Laidos",
@@ -76,65 +76,67 @@
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
"Sync": "Sinchronizuoti",
- "System": "System",
- "TvShows": "TV Serialai",
- "User": "User",
- "UserCreatedWithName": "Vartotojas {0} buvo sukurtas",
- "UserDeletedWithName": "Vartotojas {0} ištrintas",
+ "System": "Sistema",
+ "TvShows": "TV laidos",
+ "User": "Naudotojas",
+ "UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
+ "UserDeletedWithName": "Naudotojas {0} ištrintas",
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
- "UserLockedOutWithName": "Vartotojas {0} užblokuotas",
+ "UserLockedOutWithName": "Naudotojas {0} užblokuotas",
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
- "UserPasswordChangedWithName": "Slaptažodis pakeistas vartotojui {0}",
- "UserPolicyUpdatedWithName": "Vartotojo {0} teisės buvo pakeistos",
+ "UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
+ "UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos",
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
"ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
- "ValueSpecialEpisodeName": "Ypatinga - {0}",
- "VersionNumber": "Version {0}",
- "TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
- "TaskUpdatePlugins": "Atnaujinti Priedus",
+ "ValueSpecialEpisodeName": "Ypatingų - {0}",
+ "VersionNumber": "Versija {0}",
+ "TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
+ "TaskUpdatePlugins": "Atnaujinti įskieius",
"TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
- "TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
- "TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
- "TaskRefreshLibrary": "Skenuoti Mediateka",
+ "TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
+ "TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
+ "TaskRefreshLibrary": "Skenuoti medijos biblioteką",
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
"TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
"TaskRefreshChannels": "Atnaujinti kanalus",
- "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
- "TaskRefreshPeople": "Atnaujinti Žmones",
+ "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų medijos bibliotekoje.",
+ "TaskRefreshPeople": "Atnaujinti žmones",
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
- "TaskCleanLogs": "Išvalyti Žurnalą",
- "TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.",
- "TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus",
- "TaskCleanCache": "Išvalyti Talpyklą",
+ "TaskCleanLogs": "Išvalyti žurnalą",
+ "TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
+ "TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
+ "TaskCleanCache": "Išvalyti talpyklą",
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
- "TasksChannelsCategory": "Internetiniai Kanalai",
+ "TasksChannelsCategory": "Internetiniai kanalai",
"TasksApplicationCategory": "Programa",
- "TasksLibraryCategory": "Mediateka",
+ "TasksLibraryCategory": "Biblioteka",
"TasksMaintenanceCategory": "Priežiūra",
"TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
"Undefined": "Neapibrėžtas",
- "Forced": "Priverstas",
+ "Forced": "Priverstinis",
"Default": "Numatytas",
- "TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.",
+ "TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
"TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
- "TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas",
+ "TaskKeyframeExtractor": "Reikšminių kadrų (KeyFrame) išgavėjas",
"TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.",
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
- "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose",
- "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.",
- "TaskAudioNormalization": "Garso Normalizavimas",
- "TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.",
+ "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis rinkiniuose ir grojaraščiuose",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš rinkinių ir grojaraščių.",
+ "TaskAudioNormalization": "Garso normalizavimas",
+ "TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
- "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.",
+ "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
"TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
- "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius"
+ "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
+ "CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
+ "CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index 5a99d8c53..9cfeb407b 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -130,5 +130,7 @@
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
- "TaskAudioNormalization": "ऑडिओ सामान्यीकरण"
+ "TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
+ "TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
+ "TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 8d8b32f81..971f79c2c 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -1,10 +1,10 @@
{
"Albums": "Album",
- "AppDeviceValues": "Apl: {0}, Peranti: {1}",
+ "AppDeviceValues": "Aplikasi: {0}, Peranti: {1}",
"Application": "Aplikasi",
- "Artists": "Artis-artis",
+ "Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
- "Books": "Buku-buku",
+ "Books": "Buku",
"CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
"Channels": "Saluran",
"ChapterNameValue": "Bab {0}",
@@ -99,7 +99,7 @@
"TasksMaintenanceCategory": "Penyelenggaraan",
"Undefined": "Tidak ditentukan",
"Forced": "Dipaksa",
- "Default": "Lalai",
+ "Default": "Default",
"TaskCleanCache": "Bersihkan Direktori Cache",
"TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.",
"TaskRefreshPeople": "Segarkan Orang",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index c00eb467f..8baa63d89 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -135,6 +135,6 @@
"TaskDownloadMissingLyricsDescription": "Last ned sangtekster",
"TaskExtractMediaSegments": "Skann mediasegment",
"TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay",
- "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.",
+ "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 50345c68e..09246bd11 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -137,6 +137,6 @@
"TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren",
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
- "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig is.",
+ "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
"CleanupUserDataTask": "Opruimtaak gebruikersdata"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index ff6376258..feb5fe215 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -119,5 +119,7 @@
"Forced": "Tvungen",
"Default": "Standard",
"External": "Ekstern",
- "HearingImpaired": "Nedsett høyrsel"
+ "HearingImpaired": "Nedsett høyrsel",
+ "TaskRefreshTrickplayImages": "Generer Trickplay-bilete",
+ "TaskAudioNormalization": "Normalisering av lyd"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 26dc5ce82..c5ca6b64a 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -32,5 +32,14 @@
"HeaderFavoriteShows": "Treasured Tales",
"ChapterNameValue": "Piece {0}",
"HeaderFavoriteSongs": "Treasured Chimes",
- "HeaderNextUp": "Incoming"
+ "HeaderNextUp": "Incoming",
+ "HeaderLiveTV": "Scrying Glass",
+ "HearingImpaired": "Hard o' Hearing",
+ "LabelRunningTimeValue": "Journey duration: {0}",
+ "MessageApplicationUpdated": "Yer Map of the Seas has been scribbled",
+ "HomeVideos": "Yer Onboard Booty",
+ "MixedContent": "Jumbled loot",
+ "Music": "Tunes",
+ "NameInstallFailed": "Ye couldn't bring {0} aboard yer ship",
+ "MessageApplicationUpdatedTo": "Yer Map of the Seas has been scribbled with {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 6c49481c3..f188822d6 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
- "TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca."
+ "TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
+ "CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
+ "CleanupUserDataTask": "Limpeza de dados de utilizador"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 52427f24b..74bb1c63a 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -135,5 +135,7 @@
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
- "TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay"
+ "TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
+ "CleanupUserDataTask": "Task de limpeza de dados do usuário",
+ "CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index a873c157e..bf71c5afa 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -98,7 +98,7 @@
"TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.",
"TaskCleanTranscode": "Curățați directorul de transcodare",
"TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.",
- "TaskUpdatePlugins": "Actualizați Extensile",
+ "TaskUpdatePlugins": "Actualizați Extensiile",
"TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.",
"TaskRefreshPeople": "Actualizează Persoanele",
"TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.",
@@ -135,5 +135,7 @@
"TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.",
"TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay",
"TaskDownloadMissingLyrics": "Descarcă versurile lipsă",
- "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii"
+ "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii",
+ "CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului",
+ "CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index b17e7ae55..ff92db2f2 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -136,5 +136,7 @@
"TaskCleanCollectionsAndPlaylists": "Počisti zbirke in sezname predvajanja",
"TaskAudioNormalization": "Normalizacija zvoka",
"TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več.",
+ "CleanupUserDataTask": "Čiščenje uporabniških podatkov",
+ "CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo."
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 3e3ca5a1a..478111049 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -98,8 +98,8 @@
"TasksLibraryCategory": "Kütüphane",
"TasksMaintenanceCategory": "Bakım",
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
- "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
- "TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
+ "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik alt yazılar için internette arama yapar.",
+ "TaskDownloadMissingSubtitles": "Eksik alt yazıları indir",
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
"TaskRefreshChannels": "Kanalları Yenile",
"TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json
index 150fb7126..e44b3f516 100644
--- a/Emby.Server.Implementations/Localization/Core/uz.json
+++ b/Emby.Server.Implementations/Localization/Core/uz.json
@@ -110,5 +110,6 @@
"TaskCleanCache": "Kesh katalogini tozalash",
"TaskRefreshChapterImages": "Sahnadan tasvirini chiqarish",
"TaskRefreshChapterImagesDescription": "Sahnalarni o'z ichiga olgan videolar uchun eskizlarni yaratadi.",
- "TaskRefreshLibrary": "Media kutubxonangizni skanerlash"
+ "TaskRefreshLibrary": "Media kutubxonangizni skanerlash",
+ "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi."
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 286efb7e9..39141d841 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -136,5 +136,6 @@
"TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。",
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
- "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置"
+ "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
+ "CleanupUserDataTask": "用戶資料清理工作"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index a4ee68fc4..b3bb9106b 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -5,23 +5,23 @@
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "成功授權 {0}",
"Books": "書籍",
- "CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片",
+ "CameraImageUploadedFrom": "已從 {0} 成功上傳一張照片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "系列作",
"DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
- "FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試",
+ "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗嘗試",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯",
- "HeaderFavoriteArtists": "最愛藝人",
- "HeaderFavoriteEpisodes": "最愛劇集",
- "HeaderFavoriteShows": "最愛節目",
- "HeaderFavoriteSongs": "最愛歌曲",
+ "HeaderFavoriteArtists": "最愛的藝人",
+ "HeaderFavoriteEpisodes": "最愛的劇集",
+ "HeaderFavoriteShows": "最愛的節目",
+ "HeaderFavoriteSongs": "最愛的歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "接下來",
"HomeVideos": "家庭影片",
@@ -135,5 +135,7 @@
"TaskExtractMediaSegments": "掃描媒體片段",
"TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。",
"TaskMoveTrickplayImages": "遷移快轉縮圖位置",
- "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。"
+ "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。",
+ "CleanupUserDataTask": "用戶資料清理工作",
+ "CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 242f2af56..b4c65ad85 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -128,7 +128,8 @@ namespace Emby.Server.Implementations.Localization
}
string name = parts[3];
- if (string.IsNullOrWhiteSpace(name))
+ string displayname = parts[3];
+ if (string.IsNullOrWhiteSpace(displayname))
{
continue;
}
@@ -138,6 +139,10 @@ namespace Emby.Server.Implementations.Localization
{
continue;
}
+ else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase))
+ {
+ name = twoCharName;
+ }
string[] threeLetterNames;
if (string.IsNullOrWhiteSpace(parts[1]))
@@ -153,7 +158,7 @@ namespace Emby.Server.Implementations.Localization
iso6392BtoTdict.TryAdd(parts[1], parts[0]);
}
- list.Add(new CultureDto(name, name, twoCharName, threeLetterNames));
+ list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
}
_cultures = list;
diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt
index 42bb46d7d..5e65bae26 100644
--- a/Emby.Server.Implementations/Localization/iso6392.txt
+++ b/Emby.Server.Implementations/Localization/iso6392.txt
@@ -311,8 +311,8 @@ nia|||Nias|nias
nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues
niu|||Niuean|niué
nld|dut|nl|Dutch; Flemish|néerlandais; flamand
-nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien
-nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål
+nno||nn|Norwegian (Nynorsk)|norvégien (nynorsk)
+nob||nb|Norwegian (Bokmal)|norvégien (bokmål)
nog|||Nogai|nogaï; nogay
non|||Norse, Old|norrois, vieux
nor||no|Norwegian|norvégien
@@ -373,7 +373,7 @@ sam|||Samaritan Aramaic|samaritain
san||sa|Sanskrit|sanskrit
sas|||Sasak|sasak
sat|||Santali|santal
-scc|srp|sr|Serbian|serbe
+srp||sr|Serbian|serbe
scn|||Sicilian|sicilien
sco|||Scots|écossais
sel|||Selkup|selkoupe
@@ -391,10 +391,10 @@ slv||sl|Slovenian|slovène
sma|||Southern Sami|sami du Sud
sme||se|Northern Sami|sami du Nord
smi|||Sami languages|sames, langues
-smj|||Lule Sami|sami de Lule
-smn|||Inari Sami|sami d'Inari
+smj|||Sami (Lule)|sami de Lule
+smn|||Sami (Inari)|sami d'Inari
smo||sm|Samoan|samoan
-sms|||Skolt Sami|sami skolt
+sms|||Sami (Skolt)|sami skolt
sna||sn|Shona|shona
snd||sd|Sindhi|sindhi
snk|||Soninke|soninké
@@ -402,7 +402,7 @@ sog|||Sogdian|sogdien
som||so|Somali|somali
son|||Songhai languages|songhai, langues
sot||st|Sotho, Southern|sotho du Sud
-spa||es-mx|Spanish; Latin|espagnol; Latin
+spa||es-419|Spanish; Latin|espagnol; Latin
spa||es|Spanish; Castilian|espagnol; castillan
sqi|alb|sq|Albanian|albanais
srd||sc|Sardinian|sarde
@@ -483,9 +483,12 @@ zen|||Zenaga|zenaga
zgh|||Standard Moroccan Tamazight|amazighe standard marocain
zha||za|Zhuang; Chuang|zhuang; chuang
zho|chi|zh|Chinese|chinois
-zho|chi|ze|Chinese; Bilingual|chinois
-zho|chi|zh-tw|Chinese; Traditional|chinois
-zho|chi|zh-hk|Chinese; Hong Kong|chinois
+zho|chi|ze|Chinese (Bilingual)|chinois
+zho|chi|zh-cn|Chinese (Simplified)|chinois
+zho|chi|zh-hans|Chinese (Simplified)|chinois
+zho|chi|zh-tw|Chinese (Traditional)|chinois
+zho|chi|zh-hant|Chinese (Traditional)|chinois
+zho|chi|zh-hk|Chinese (Hong Kong)|chinois
znd|||Zande languages|zandé, langues
zul||zu|Zulu|zoulou
zun|||Zuni|zuni
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index ef005bfaa..e912e9f01 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -76,81 +76,98 @@ public partial class AudioNormalizationTask : IScheduledTask
/// <inheritdoc />
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
- foreach (var library in _libraryManager.RootFolder.Children)
+ var numComplete = 0;
+ var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray();
+ double percent = 0;
+
+ foreach (var library in libraries)
{
- var libraryOptions = _libraryManager.GetLibraryOptions(library);
- if (!libraryOptions.EnableLUFSScan)
- {
- continue;
- }
+ var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true });
- // Album gain
- var albums = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = [BaseItemKind.MusicAlbum],
- Parent = library,
- Recursive = true
- });
+ double nextPercent = numComplete + 1;
+ nextPercent /= libraries.Length;
+ nextPercent -= percent;
+ // Split the progress for this single library into two halves: album gain and track gain.
+ // The first half will be for album gain, the second half for track gain.
+ nextPercent /= 2;
+ var albumComplete = 0;
foreach (var a in albums)
{
- if (a.NormalizationGain.HasValue || a.LUFS.HasValue)
+ if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue)
{
- continue;
+ // Album gain
+ var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
+
+ // Skip albums that don't have multiple tracks, album gain is useless here
+ if (albumTracks.Count > 1)
+ {
+ _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
+ var tempDir = _applicationPaths.TempDirectory;
+ Directory.CreateDirectory(tempDir);
+ var tempFile = Path.Join(tempDir, a.Id + ".concat");
+ var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
+ await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ a.LUFS = await CalculateLUFSAsync(
+ string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
+ OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
+ cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
}
- // Skip albums that don't have multiple tracks, album gain is useless here
- var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
- if (albumTracks.Count <= 1)
- {
- continue;
- }
+ // Update sub-progress for album gain
+ albumComplete++;
+ double albumPercent = albumComplete;
+ albumPercent /= albums.Count;
- _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
- var tempDir = _applicationPaths.TempDirectory;
- Directory.CreateDirectory(tempDir);
- var tempFile = Path.Join(tempDir, a.Id + ".concat");
- var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
- await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
- try
- {
- a.LUFS = await CalculateLUFSAsync(
- string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
- OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
- cancellationToken).ConfigureAwait(false);
- }
- finally
- {
- File.Delete(tempFile);
- }
+ progress.Report(100 * (percent + (albumPercent * nextPercent)));
}
+ // Update progress to start at the track gain percent calculation
+ percent += nextPercent;
+
_itemRepository.SaveItems(albums, cancellationToken);
// Track gain
- var tracks = _libraryManager.GetItemList(new InternalItemsQuery
- {
- MediaTypes = [MediaType.Audio],
- IncludeItemTypes = [BaseItemKind.Audio],
- Parent = library,
- Recursive = true
- });
+ var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true });
+ var tracksComplete = 0;
foreach (var t in tracks)
{
- if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)
+ if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
{
- continue;
+ t.LUFS = await CalculateLUFSAsync(
+ string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
+ false,
+ cancellationToken).ConfigureAwait(false);
}
- t.LUFS = await CalculateLUFSAsync(
- string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
- false,
- cancellationToken).ConfigureAwait(false);
+ // Update sub-progress for track gain
+ tracksComplete++;
+ double trackPercent = tracksComplete;
+ trackPercent /= tracks.Count;
+
+ progress.Report(100 * (percent + (trackPercent * nextPercent)));
}
_itemRepository.SaveItems(tracks, cancellationToken);
+
+ // Update progress
+ numComplete++;
+ percent = numComplete;
+ percent /= libraries.Length;
+
+ progress.Report(100 * percent);
}
+
+ progress.Report(100.0);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index d4606abd2..6a26e92e1 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -5,6 +5,8 @@ using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Controller.Session;
@@ -44,6 +46,7 @@ namespace Emby.Server.Implementations.Session
private readonly Lock _webSocketsLock = new();
private readonly ISessionManager _sessionManager;
+ private readonly IUserManager _userManager;
private readonly ILogger<SessionWebSocketListener> _logger;
private readonly ILoggerFactory _loggerFactory;
@@ -57,14 +60,17 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
+ /// <param name="userManager">The user manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
+ IUserManager userManager,
ILoggerFactory loggerFactory)
{
_logger = logger;
_sessionManager = sessionManager;
+ _userManager = userManager;
_loggerFactory = loggerFactory;
_keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
{
@@ -107,33 +113,9 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext)
{
- var session = await GetSession(httpContext, connection.RemoteEndPoint?.ToString()).ConfigureAwait(false);
- if (session is not null)
- {
- EnsureController(session, connection);
- await KeepAliveWebSocket(connection).ConfigureAwait(false);
- }
- else
- {
- _logger.LogWarning("Unable to determine session based on query string: {0}", httpContext.Request.QueryString);
- }
- }
-
- private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint)
- {
- if (!httpContext.User.Identity?.IsAuthenticated ?? false)
- {
- return null;
- }
-
- var deviceId = httpContext.User.GetDeviceId();
- if (httpContext.Request.Query.TryGetValue("deviceId", out var queryDeviceId))
- {
- deviceId = queryDeviceId;
- }
-
- return await _sessionManager.GetSessionByAuthenticationToken(httpContext.User.GetToken(), deviceId, remoteEndpoint)
- .ConfigureAwait(false);
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, httpContext).ConfigureAwait(false);
+ EnsureController(session, connection);
+ await KeepAliveWebSocket(connection).ConfigureAwait(false);
}
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 4cac8ed67..2614fe995 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -46,6 +46,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1);
+ private readonly Version _minFFmpegHlsSegmentOptions = new Version(5, 0);
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
@@ -1606,6 +1607,7 @@ public class DynamicHlsController : BaseJellyfinApiController
var segmentFormat = string.Empty;
var segmentContainer = outputExtension.TrimStart('.');
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
+ var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
{
@@ -1621,6 +1623,11 @@ public class DynamicHlsController : BaseJellyfinApiController
false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
};
+ var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
+
+ // fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
+ hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
+
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}
else
@@ -1642,8 +1649,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Path.GetFileNameWithoutExtension(outputPath));
}
- var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
-
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"",
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 50eeaeac6..e1d9b6bba 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -158,7 +158,10 @@ public class ItemUpdateController : BaseJellyfinApiController
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
Countries = _localizationManager.GetCountries().ToArray(),
- Cultures = _localizationManager.GetCultures().ToArray()
+ Cultures = _localizationManager.GetCultures()
+ .DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
+ .OrderBy(c => c.DisplayName)
+ .ToArray()
};
if (!item.IsVirtualItem
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index bbce5a9e1..dd8f935dc 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -1,4 +1,6 @@
+using System;
using System.Collections.Generic;
+using System.Linq;
using MediaBrowser.Common.Api;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -34,7 +36,14 @@ public class LocalizationController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<CultureDto>> GetCultures()
{
- return Ok(_localization.GetCultures());
+ var allCultures = _localization.GetCultures();
+
+ var distinctCultures = allCultures
+ .DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
+ .OrderBy(c => c.DisplayName)
+ .AsEnumerable();
+
+ return Ok(distinctCultures);
}
/// <summary>
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 0690f0c8d..4034a8088 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -32,17 +32,67 @@ public static class FileStreamResponseHelpers
HttpContext httpContext,
CancellationToken cancellationToken = default)
{
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(state.MediaPath));
+
+ // Forward User-Agent if provided
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
{
- httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
+ // Clear default and add specific one if exists, otherwise HttpClient default might be used
+ requestMessage.Headers.UserAgent.Clear();
+ requestMessage.Headers.TryAddWithoutValidation(HeaderNames.UserAgent, useragent);
+ }
+
+ // Forward Range header if present in the client request
+ if (httpContext.Request.Headers.TryGetValue(HeaderNames.Range, out var rangeValue))
+ {
+ var rangeString = rangeValue.ToString();
+ if (!string.IsNullOrEmpty(rangeString))
+ {
+ requestMessage.Headers.Range = System.Net.Http.Headers.RangeHeaderValue.Parse(rangeString);
+ }
}
- // Can't dispose the response as it's required up the call chain.
- var response = await httpClient.GetAsync(new Uri(state.MediaPath), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
+ // Send the request to the upstream server
+ // Use ResponseHeadersRead to avoid downloading the whole content immediately
+ var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
+ // Check if the upstream server supports range requests and acted upon our Range header
+ bool upstreamSupportsRange = response.StatusCode == System.Net.HttpStatusCode.PartialContent;
+ string acceptRangesValue = "none";
+ if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders))
+ {
+ // Prefer upstream server's Accept-Ranges header if available
+ acceptRangesValue = string.Join(", ", acceptRangesHeaders);
+ upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase);
+ }
+ else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes
+ {
+ acceptRangesValue = "bytes";
+ }
+
+ // Set Accept-Ranges header for the client based on upstream support
+ httpContext.Response.Headers[HeaderNames.AcceptRanges] = acceptRangesValue;
+
+ // Set Content-Range header if upstream provided it (implies partial content)
+ if (response.Content.Headers.ContentRange is not null)
+ {
+ httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString();
+ }
+
+ // Set Content-Length header. For partial content, this is the length of the partial segment.
+ if (response.Content.Headers.ContentLength.HasValue)
+ {
+ httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value;
+ }
+
+ // Set Content-Type header
+ var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Application.Octet; // Use a more generic default
+
+ // Set the status code for the client response (e.g., 200 OK or 206 Partial Content)
+ httpContext.Response.StatusCode = (int)response.StatusCode;
+ // Return the stream from the upstream server
+ // IMPORTANT: Do not dispose the response stream here, FileStreamResult will handle it.
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index e10e940f2..5072f902d 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -111,7 +111,16 @@ public static class RequestHelpers
return user.EnableUserPreferenceAccess;
}
- internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
+ /// <summary>
+ /// Get the session based on http request.
+ /// </summary>
+ /// <param name="sessionManager">The session manager.</param>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="httpContext">The http context.</param>
+ /// <param name="userId">The optional userid.</param>
+ /// <returns>The session.</returns>
+ /// <exception cref="ResourceNotFoundException">Session not found.</exception>
+ public static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
{
userId ??= httpContext.User.GetUserId();
User? user = null;
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index 63c80634f..932f9d625 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -139,7 +139,7 @@ public static class ServiceCollectionExtensions
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var provider = serviceProvider.GetRequiredService<IJellyfinDatabaseProvider>();
- provider.Initialise(opt);
+ provider.Initialise(opt, efCoreConfiguration);
var lockingBehavior = serviceProvider.GetRequiredService<IEntityFrameworkCoreLockingBehavior>();
lockingBehavior.Initialise(opt);
});
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
index d439fcb18..74d99455d 100644
--- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
+++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
@@ -39,7 +39,7 @@ public class BackupService : IBackupService
ReferenceHandler = ReferenceHandler.IgnoreCycles,
};
- private readonly Version _backupEngineVersion = Version.Parse("0.1.0");
+ private readonly Version _backupEngineVersion = Version.Parse("0.2.0");
/// <summary>
/// Initializes a new instance of the <see cref="BackupService"/> class.
@@ -120,26 +120,29 @@ public class BackupService : IBackupService
void CopyDirectory(string source, string target)
{
- source = Path.GetFullPath(source);
- Directory.CreateDirectory(source);
-
+ var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
+ var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
foreach (var item in zipArchive.Entries)
{
- var sanitizedSourcePath = Path.GetFullPath(item.FullName.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
- if (!sanitizedSourcePath.StartsWith(target, StringComparison.Ordinal))
+ var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
+ var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
+
+ if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
+ || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
{
continue;
}
- var targetPath = Path.Combine(source, sanitizedSourcePath[target.Length..].Trim('/'));
_logger.LogInformation("Restore and override {File}", targetPath);
- item.ExtractToFile(targetPath);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
+ item.ExtractToFile(targetPath, overwrite: true);
}
}
- CopyDirectory(_applicationPaths.ConfigurationDirectoryPath, "Config/");
- CopyDirectory(_applicationPaths.DataPath, "Data/");
- CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
+ CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
+ CopyDirectory("Data", _applicationPaths.DataPath);
+ CopyDirectory("Root", _applicationPaths.RootFolderPath);
if (manifest.Options.Database)
{
@@ -148,7 +151,7 @@ public class BackupService : IBackupService
await using (dbContext.ConfigureAwait(false))
{
// restore migration history manually
- var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json");
+ var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{nameof(HistoryRow)}.json")));
if (historyEntry is null)
{
_logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation");
@@ -165,6 +168,13 @@ public class BackupService : IBackupService
var historyRepository = dbContext.GetService<IHistoryRepository>();
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
+
+ foreach (var item in await historyRepository.GetAppliedMigrationsAsync(CancellationToken.None).ConfigureAwait(false))
+ {
+ var insertScript = historyRepository.GetDeleteScript(item.MigrationId);
+ await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
+ }
+
foreach (var item in historyEntries)
{
var insertScript = historyRepository.GetInsertScript(item);
@@ -186,7 +196,7 @@ public class BackupService : IBackupService
{
_logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
- var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
+ var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
if (zipEntry is null)
{
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
@@ -198,7 +208,7 @@ public class BackupService : IBackupService
{
_logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
var records = 0;
- await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!)
+ await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false))
{
var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
if (entity is null)
@@ -281,7 +291,7 @@ public class BackupService : IBackupService
await using (dbContext.ConfigureAwait(false))
{
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
- static IAsyncEnumerable<object> GetValues(IQueryable dbSet, Type type)
+ static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
{
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
var enumerable = method.Invoke(dbSet, null)!;
@@ -296,8 +306,8 @@ public class BackupService : IBackupService
.. typeof(JellyfinDbContext)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
- .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
- (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
+ .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
+ (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
@@ -309,7 +319,7 @@ public class BackupService : IBackupService
foreach (var entityType in entityTypes)
{
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
- var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.SourceName}.json");
+ var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
var entities = 0;
var zipEntryStream = zipEntry.Open();
await using (zipEntryStream.ConfigureAwait(false))
@@ -347,7 +357,7 @@ public class BackupService : IBackupService
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
{
- zipArchive.CreateEntryFromFile(item, Path.Combine("Config", Path.GetFileName(item)));
+ zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
}
void CopyDirectory(string source, string target, string filter = "*")
@@ -361,7 +371,7 @@ public class BackupService : IBackupService
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
{
- zipArchive.CreateEntryFromFile(item, Path.Combine(target, item[..source.Length].Trim('\\')));
+ zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
}
}
@@ -509,4 +519,14 @@ public class BackupService : IBackupService
Database = options.Database
};
}
+
+ /// <summary>
+ /// Windows is able to handle '/' as a path seperator in zip files
+ /// but linux isn't able to handle '\' as a path seperator in zip files,
+ /// So normalize to '/'.
+ /// </summary>
+ /// <param name="path">The path to normalize.</param>
+ /// <returns>The normalized path. </returns>
+ private static string NormalizePathSeparator(string path)
+ => path.Replace('\\', '/');
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 52585c996..cf4f405ee 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -110,6 +110,20 @@ public sealed class BaseItemRepository
using var transaction = context.Database.BeginTransaction();
var date = (DateTime?)DateTime.UtcNow;
+
+ // Remove any UserData entries for the placeholder item that would conflict with the UserData
+ // being detached from the item being deleted. This is necessary because, during an update,
+ // UserData may be reattached to a new entry, but some entries can be left behind.
+ // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
+ context.UserData
+ .Join(
+ context.UserData.Where(e => e.ItemId == id),
+ placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
+ userData => new { userData.UserId, userData.CustomDataKey },
+ (placeholder, userData) => placeholder)
+ .Where(e => e.ItemId == PlaceholderId)
+ .ExecuteDelete();
+
// Detach all user watch data
context.UserData.Where(e => e.ItemId == id)
.ExecuteUpdate(e => e
@@ -443,6 +457,66 @@ public sealed class BaseItemRepository
return dbQuery.Count();
}
+ /// <inheritdoc />
+ public ItemCounts GetItemCounts(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ // Hack for right now since we currently don't support filtering out these duplicates within a query
+ PrepareFilterQuery(filter);
+
+ using var context = _dbProvider.CreateDbContext();
+ var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
+
+ var counts = dbQuery
+ .GroupBy(x => x.Type)
+ .Select(x => new { x.Key, Count = x.Count() })
+ .AsEnumerable();
+
+ var lookup = _itemTypeLookup.BaseItemKindNames;
+ var result = new ItemCounts();
+ foreach (var count in counts)
+ {
+ if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
+ {
+ result.AlbumCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
+ {
+ result.ArtistCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
+ {
+ result.EpisodeCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
+ {
+ result.MovieCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
+ {
+ result.MusicVideoCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
+ {
+ result.ProgramCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
+ {
+ result.SeriesCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
+ {
+ result.SongCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
+ {
+ result.TrailerCount = count.Count;
+ }
+ }
+
+ return result;
+ }
+
#pragma warning disable CA1307 // Specify StringComparison for clarity
/// <summary>
/// Gets the type.
@@ -468,6 +542,13 @@ public sealed class BaseItemRepository
var images = item.ImageInfos.Select(e => Map(item.Id, e));
using var context = _dbProvider.CreateDbContext();
+
+ if (!context.BaseItems.Any(bi => bi.Id == item.Id))
+ {
+ _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
+ return;
+ }
+
context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
context.BaseItemImageInfos.AddRange(images);
context.SaveChanges();
@@ -567,7 +648,7 @@ public sealed class BaseItemRepository
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
var valueMap = itemValueMaps
- .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray()))
+ .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).DistinctBy(e => e.ItemValueId).ToArray()))
.ToArray();
var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
@@ -1094,13 +1175,18 @@ public sealed class BaseItemRepository
IsSeries = filter.IsSeries
});
+ var itemValuesQuery = context.ItemValues
+ .Where(f => itemValueTypes.Contains(f.Type))
+ .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
+ .Join(
+ innerQueryFilter,
+ fw => fw.w.ItemId,
+ g => g.Id,
+ (fw, g) => fw.f.CleanValue);
+
var innerQuery = PrepareItemQuery(context, filter)
.Where(e => e.Type == returnType)
- .Where(e => context.ItemValues!
- .Where(f => itemValueTypes.Contains(f.Type))
- .Where(f => innerQueryFilter.Any(g => f.BaseItemsMap!.Any(w => w.ItemId == g.Id)))
- .Select(f => f.CleanValue)
- .Contains(e.CleanName));
+ .Where(e => itemValuesQuery.Contains(e.CleanName));
var outerQueryFilter = new InternalItemsQuery(filter.User)
{
@@ -1881,7 +1967,7 @@ public sealed class BaseItemRepository
if (filter.AlbumArtistIds.Length > 0)
{
- baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.AlbumArtistIds);
+ baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
}
if (filter.ContributingArtistIds.Length > 0)
@@ -2239,8 +2325,8 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0)
{
baseQuery = baseQuery
- .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
- .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
+ .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)
+ .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
if (filter.IncludeInheritedTags.Length > 0)
@@ -2250,10 +2336,10 @@ public sealed class BaseItemRepository
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
{
baseQuery = baseQuery
- .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
+ .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
||
- (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
+ (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags))
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
}
@@ -2261,17 +2347,16 @@ public sealed class BaseItemRepository
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
{
baseQuery = baseQuery
- .Where(e =>
- e.Parents!
- .Any(f =>
- f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
- || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
+ .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
+ .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
+ || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
// d ^^ this is stupid it hate this.
}
else
{
baseQuery = baseQuery
- .Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
+ .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
+ .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
}
diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
index 3ae6dbd70..e75dda439 100644
--- a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
@@ -25,8 +25,16 @@ public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbPr
{
using var context = dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
+
+ // Users may replace a media with a version that includes attachments to one without them.
+ // So when saving attachments is triggered by a library scan, we always unconditionally
+ // clear the old ones, and then add the new ones if given.
context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
- context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
+ if (attachments.Any())
+ {
+ context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
+ }
+
context.SaveChanges();
transaction.Commit();
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 14c4285fe..df630922a 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -53,6 +53,7 @@
<PackageReference Include="prometheus-net.AspNetCore" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Thread" />
+ <PackageReference Include="Serilog.Expressions" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.Console" />
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
index 31a220118..fe191916c 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
@@ -17,6 +17,7 @@ using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations;
@@ -105,6 +106,13 @@ internal class JellyfinMigrationService
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
+ var databaseCreator = dbContext.Database.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator
+ ?? throw new InvalidOperationException("Jellyfin does only support relational databases.");
+ if (!await databaseCreator.ExistsAsync().ConfigureAwait(false))
+ {
+ await databaseCreator.CreateAsync().ConfigureAwait(false);
+ }
+
var historyRepository = dbContext.GetService<IHistoryRepository>();
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index e25c52786..e04a2737a 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -90,6 +90,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
}
+ // notify the other migration to just silently abort because the fix has been applied here already.
+ ReseedFolderFlag.RerunGuardFlag = true;
+
var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
connection.Open();
@@ -105,7 +108,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
- ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems
+ ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType, IsFolder FROM TypedBaseItems
""";
using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
{
@@ -1167,6 +1170,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
entity.UnratedType = unratedType;
}
+ if (reader.TryGetBoolean(index++, out var isFolder))
+ {
+ entity.IsFolder = isFolder;
+ }
+
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
index beb8c0a9b..8a0a1741f 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Controller;
@@ -78,6 +79,8 @@ internal class MigrateLibraryUserData : IAsyncMigrationRoutine
WHERE NOT EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
""");
+
+ var importedUserData = new Dictionary<Guid, List<UserData>>();
foreach (var entity in queryResult)
{
var userData = MigrateLibraryDb.GetUserData(users, entity, userIdBlacklist, _logger);
@@ -95,9 +98,22 @@ internal class MigrateLibraryUserData : IAsyncMigrationRoutine
continue;
}
+ var ogId = userData.ItemId;
userData.ItemId = BaseItemRepository.PlaceholderId;
userData.RetentionDate = retentionDate;
- dbContext.UserData.Add(userData);
+ if (!importedUserData.TryGetValue(ogId, out var importUserData))
+ {
+ importUserData = [];
+ importedUserData[ogId] = importUserData;
+ }
+
+ importUserData.Add(userData);
+ }
+
+ foreach (var item in importedUserData)
+ {
+ await dbContext.UserData.Where(e => e.ItemId == item.Key).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ dbContext.UserData.AddRange(item.Value.DistinctBy(e => e.CustomDataKey)); // old userdata can have fucked up duplicates
}
_logger.LogInformation("Try saving {NewSaved} UserData entries.", dbContext.UserData.Local.Count);
diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
new file mode 100644
index 000000000..502763ac0
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
@@ -0,0 +1,74 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Data;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+[JellyfinMigration("2025-07-30T21:50:00", nameof(ReseedFolderFlag))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+internal class ReseedFolderFlag : IAsyncMigrationRoutine
+{
+ private const string DbFilename = "library.db.old";
+
+ private readonly IStartupLogger _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+
+ public ReseedFolderFlag(
+ IStartupLogger<MigrateLibraryDb> startupLogger,
+ IDbContextFactory<JellyfinDbContext> provider,
+ IServerApplicationPaths paths)
+ {
+ _logger = startupLogger;
+ _provider = provider;
+ _paths = paths;
+ }
+
+ internal static bool RerunGuardFlag { get; set; } = false;
+
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ if (RerunGuardFlag)
+ {
+ _logger.LogInformation("Migration is skipped because it does not apply.");
+ return;
+ }
+
+ _logger.LogInformation("Migrating the IsFolder flag from library.db.old may take a while, do not stop Jellyfin.");
+
+ var dataPath = _paths.DataPath;
+ var libraryDbPath = Path.Combine(dataPath, DbFilename);
+ if (!File.Exists(libraryDbPath))
+ {
+ _logger.LogError("Cannot migrate IsFolder flag from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
+ return;
+ }
+
+ var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
+ var queryResult = connection.Query(
+ """
+ SELECT guid FROM TypedBaseItems
+ WHERE IsFolder = true
+ """)
+ .Select(entity => entity.GetGuid(0))
+ .ToList();
+ _logger.LogInformation("Migrating the IsFolder flag for {Count} items.", queryResult.Count);
+ foreach (var id in queryResult)
+ {
+ await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
index c3592f62a..264710bce 100644
--- a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
+++ b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
@@ -82,7 +82,7 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
}
}
- private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>, IStartupLogger<TCategory>
+ private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>
{
public NestedStartupLogger(ILogger logger, StartupLogTopic topic) : base(logger, topic)
{
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 4efa3f410..bb0b26b8e 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1430,9 +1430,7 @@ namespace MediaBrowser.Controller.Entities
var info = FileSystem.GetFileSystemInfo(Path);
- return info.Exists
- ? info.LastWriteTimeUtc != DateModified
- : false;
+ return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
}
public virtual List<string> GetUserDataKeys()
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index dcd22a3b4..668e2c1e2 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -114,5 +114,19 @@ namespace MediaBrowser.Controller.Entities
source.DeepCopy(dest);
return dest;
}
+
+ /// <summary>
+ /// Determines if the item has changed.
+ /// </summary>
+ /// <param name="source">The source object.</param>
+ /// <param name="asOf">The timestamp to detect changes as of.</param>
+ /// <typeparam name="T">Source type.</typeparam>
+ /// <returns>Whether the item has changed.</returns>
+ public static bool HasChanged<T>(this T source, DateTime asOf)
+ where T : BaseItem
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ return source.DateModified.Subtract(asOf).Duration().TotalSeconds > 1;
+ }
}
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 732a03a2a..06cbcc2e1 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -695,7 +695,7 @@ namespace MediaBrowser.Controller.Entities
items = GetRecursiveChildren(user, query);
}
- return PostFilterAndSort(items, query, true);
+ return PostFilterAndSort(items, query);
}
if (this is not UserRootFolder
@@ -959,10 +959,10 @@ namespace MediaBrowser.Controller.Entities
items = GetChildren(user, true, childQuery).Where(filter);
}
- return PostFilterAndSort(items, query, true);
+ return PostFilterAndSort(items, query);
}
- protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
+ protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
{
var user = query.User;
@@ -995,7 +995,7 @@ namespace MediaBrowser.Controller.Entities
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
- return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
+ return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
}
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index d50f3d075..b32b64f5d 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Diacritics.Extensions;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
@@ -373,8 +374,15 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i != other)
.Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
- ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
- IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
+ ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags)
+ .Where(tag => !string.IsNullOrWhiteSpace(tag))
+ .Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
+ .ToArray();
+
+ IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags)
+ .Where(tag => !string.IsNullOrWhiteSpace(tag))
+ .Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
+ .ToArray();
User = user;
}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 408161b03..48211d99f 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -179,7 +179,7 @@ namespace MediaBrowser.Controller.Entities.TV
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
- return PostFilterAndSort(items, query, false);
+ return PostFilterAndSort(items, query);
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index bc7e22d9a..deed3631b 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.Entities
PresetViews = query.PresetViews
});
- return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true);
+ return UserViewBuilder.SortAndPage(result, null, query, LibraryManager);
}
public override int GetChildCount(User user)
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 1eb3c8f50..7679d383f 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -438,22 +438,18 @@ namespace MediaBrowser.Controller.Entities
items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
- return SortAndPage(items, totalRecordLimit, query, libraryManager, true);
+ return SortAndPage(items, totalRecordLimit, query, libraryManager);
}
public static QueryResult<BaseItem> SortAndPage(
IEnumerable<BaseItem> items,
int? totalRecordLimit,
InternalItemsQuery query,
- ILibraryManager libraryManager,
- bool enableSorting)
+ ILibraryManager libraryManager)
{
- if (enableSorting)
+ if (query.OrderBy.Count > 0)
{
- if (query.OrderBy.Count > 0)
- {
- items = libraryManager.Sort(items, query.User, query.OrderBy);
- }
+ items = libraryManager.Sort(items, query.User, query.OrderBy);
}
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 98ed15eb6..b72d1d0b4 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -630,6 +630,8 @@ namespace MediaBrowser.Controller.Library
int GetCount(InternalItemsQuery query);
+ ItemCounts GetItemCounts(InternalItemsQuery query);
+
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
index f1428d6b9..0de5f198d 100644
--- a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
+++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
@@ -96,14 +96,33 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
}
}
+ private bool ShouldForceSequentialOperation()
+ {
+ // if the user either set the setting to 1 or it's unset and we have fewer than 4 cores it's better to run sequentially.
+ var fanoutSetting = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
+ return fanoutSetting == 1 || (fanoutSetting <= 0 && Environment.ProcessorCount <= 3);
+ }
+
+ private int CalculateScanConcurrencyLimit()
+ {
+ // when this is invoked, we already checked ShouldForceSequentialOperation for the sequential check.
+ var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
+ if (fanoutConcurrency <= 0)
+ {
+ // in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation.
+ return Environment.ProcessorCount - 3;
+ }
+
+ return fanoutConcurrency;
+ }
+
private void Worker()
{
lock (_taskLock)
{
- var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
- var parallelism = (fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount) - _taskRunners.Count;
- _logger.LogDebug("Spawn {NumberRunners} new runners.", parallelism);
- for (int i = 0; i < parallelism; i++)
+ var operationFanout = Math.Max(0, CalculateScanConcurrencyLimit() - _taskRunners.Count);
+ _logger.LogDebug("Spawn {NumberRunners} new runners.", operationFanout);
+ for (int i = 0; i < operationFanout; i++)
{
var stopToken = new CancellationTokenSource();
var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping);
@@ -223,7 +242,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
};
}).ToArray();
- if (_serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency == 1)
+ if (ShouldForceSequentialOperation())
{
_logger.LogDebug("Process sequentially.");
try
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index cd805f3dc..8d3977103 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -230,10 +230,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var hwType = encodingOptions.HardwareAccelerationType;
- // Only Intel has VA-API MJPEG encoder
+ // Only enable VA-API MJPEG encoder on Intel iHD driver.
+ // Legacy platforms supported ONLY by i965 do not support MJPEG encoder.
if (hwType == HardwareAccelerationType.vaapi
- && !(_mediaEncoder.IsVaapiDeviceInteliHD
- || _mediaEncoder.IsVaapiDeviceInteli965))
+ && !_mediaEncoder.IsVaapiDeviceInteliHD)
{
return _defaultMjpegEncoder;
}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index f4ac0ece4..a0dabbac6 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -84,6 +84,8 @@ public interface IItemRepository
int GetCount(InternalItemsQuery filter);
+ ItemCounts GetItemCounts(InternalItemsQuery filter);
+
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter);
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 2eb647e26..237b537bc 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -827,7 +827,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
/// <inheritdoc />
- public Task<string> ExtractVideoImagesOnIntervalAccelerated(
+ public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
string inputFile,
string container,
MediaSourceInfo mediaSource,
@@ -918,18 +918,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
inputArg = "-hwaccel_flags +low_priority " + inputArg;
}
- if (enableKeyFrameOnlyExtraction)
- {
- inputArg = "-skip_frame nokey " + inputArg;
- }
-
var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
if (string.IsNullOrWhiteSpace(filterParam))
{
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
- return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken);
+ try
+ {
+ return await ExtractVideoImagesOnIntervalInternal(
+ (enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
+ filterParam,
+ vidEncoder,
+ threads,
+ qualityScale,
+ priority,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (FfmpegException ex)
+ {
+ if (!enableKeyFrameOnlyExtraction)
+ {
+ throw;
+ }
+
+ _logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFile}", inputFile);
+ }
+
+ return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken).ConfigureAwait(false);
}
private async Task<string> ExtractVideoImagesOnIntervalInternal(
@@ -1071,11 +1087,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
-
- if (exitCode == -1)
+ if (!ranToCompletion || processWrapper.ExitCode != 0)
{
- _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
// Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
// Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
try
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 777e33587..359927d4d 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -171,14 +171,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false))
{
- var result = CharsetDetector.DetectFromStream(stream).Detected;
+ var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
+ var detected = result.Detected;
stream.Position = 0;
- if (result is not null)
+ if (detected is not null)
{
- _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, fileInfo.Path);
+ _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path);
- using var reader = new StreamReader(stream, result.Encoding);
+ using var reader = new StreamReader(stream, detected.Encoding);
var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
return new MemoryStream(Encoding.UTF8.GetBytes(text));
@@ -489,10 +490,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
var subtitleStreams = mediaSource.MediaStreams
- .Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true, IsExternal: false });
+ .Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true });
foreach (var subtitleStream in subtitleStreams)
{
+ if (subtitleStream.IsExternal && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
@@ -510,6 +516,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (extractableStreams.Count > 0)
{
await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
+ await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception ex)
@@ -522,6 +529,72 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
+ private async Task ExtractAllExtractableSubtitlesMKS(
+ MediaSourceInfo mediaSource,
+ List<MediaStream> subtitleStreams,
+ CancellationToken cancellationToken)
+ {
+ var mksFiles = new List<string>();
+
+ foreach (var subtitleStream in subtitleStreams)
+ {
+ if (string.IsNullOrEmpty(subtitleStream.Path) || !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (!mksFiles.Contains(subtitleStream.Path))
+ {
+ mksFiles.Add(subtitleStream.Path);
+ }
+ }
+
+ if (mksFiles.Count == 0)
+ {
+ return;
+ }
+
+ foreach (string mksFile in mksFiles)
+ {
+ var inputPath = _mediaEncoder.GetInputArgument(mksFile, mediaSource);
+ var outputPaths = new List<string>();
+ var args = string.Format(
+ CultureInfo.InvariantCulture,
+ "-i {0} -copyts",
+ inputPath);
+
+ foreach (var subtitleStream in subtitleStreams)
+ {
+ if (!subtitleStream.Path.Equals(mksFile, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
+ var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
+ var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
+
+ if (streamIndex == -1)
+ {
+ _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index);
+ continue;
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
+
+ outputPaths.Add(outputPath);
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0} -an -vn -c:s {1} \"{2}\"",
+ streamIndex,
+ outputCodec,
+ outputPath);
+ }
+
+ await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
private async Task ExtractAllExtractableSubtitlesInternal(
MediaSourceInfo mediaSource,
List<MediaStream> subtitleStreams,
@@ -536,6 +609,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
foreach (var subtitleStream in subtitleStreams)
{
+ if (!string.IsNullOrEmpty(subtitleStream.Path) && subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Subtitle {Index} for file {InputPath} is part in an MKS file. Skipping", inputPath, subtitleStream.Index);
+ continue;
+ }
+
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
@@ -557,6 +636,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
outputPath);
}
+ if (outputPaths.Count == 0)
+ {
+ return;
+ }
+
+ await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task ExtractSubtitlesForFile(
+ string inputPath,
+ string args,
+ List<string> outputPaths,
+ CancellationToken cancellationToken)
+ {
int exitCode;
using (var process = new Process
@@ -846,7 +939,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false))
{
- var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
+ var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
+ var charset = result.Detected?.EncodingName ?? string.Empty;
// UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal))
diff --git a/MediaBrowser.Model/Dto/ItemCounts.cs b/MediaBrowser.Model/Dto/ItemCounts.cs
index 95f4a3d77..a15a0c82a 100644
--- a/MediaBrowser.Model/Dto/ItemCounts.cs
+++ b/MediaBrowser.Model/Dto/ItemCounts.cs
@@ -76,5 +76,14 @@ namespace MediaBrowser.Model.Dto
/// </summary>
/// <value>The item count.</value>
public int ItemCount { get; set; }
+
+ /// <summary>
+ /// Adds all counts.
+ /// </summary>
+ /// <returns>The total of the counts.</returns>
+ public int TotalItemCount()
+ {
+ return MovieCount + SeriesCount + EpisodeCount + ArtistCount + ProgramCount + TrailerCount + SongCount + AlbumCount + MusicVideoCount + BoxSetCount + BookCount;
+ }
}
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 5c8f37fcd..b1626e2c9 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -273,11 +273,28 @@ namespace MediaBrowser.Model.Entities
// Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded).
if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase))
{
- // Get full language string i.e. eng -> English.
- string fullLanguage = CultureInfo
- .GetCultures(CultureTypes.NeutralCultures)
- .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
- ?.DisplayName;
+ // Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
+ var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
+ CultureInfo match = null;
+ if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
+ {
+ match = cultures.FirstOrDefault(r =>
+ r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
+
+ if (match is null)
+ {
+ string baseLang = Language.AsSpan().LeftPart('-').ToString();
+ match = cultures.FirstOrDefault(r =>
+ r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+ else
+ {
+ match = cultures.FirstOrDefault(r =>
+ r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
+ }
+
+ string fullLanguage = match?.DisplayName;
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
}
@@ -376,11 +393,28 @@ namespace MediaBrowser.Model.Entities
if (!string.IsNullOrEmpty(Language))
{
- // Get full language string i.e. eng -> English.
- string fullLanguage = CultureInfo
- .GetCultures(CultureTypes.NeutralCultures)
- .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
- ?.DisplayName;
+ // Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
+ var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
+ CultureInfo match = null;
+ if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
+ {
+ match = cultures.FirstOrDefault(r =>
+ r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
+
+ if (match is null)
+ {
+ string baseLang = Language.AsSpan().LeftPart('-').ToString();
+ match = cultures.FirstOrDefault(r =>
+ r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+ else
+ {
+ match = cultures.FirstOrDefault(r =>
+ r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
+ }
+
+ string fullLanguage = match?.DisplayName;
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
}
else
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index de087d0e7..79f8675cb 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -56,6 +56,7 @@ namespace MediaBrowser.Model.Net
".rec",
".ts",
".rmvb",
+ ".vob",
".webm",
".wmv",
".wtv",
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 5c855328b..0f2188aa8 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -332,13 +332,11 @@ namespace MediaBrowser.Providers.Manager
if (!string.IsNullOrEmpty(itemPath))
{
var info = FileSystem.GetFileSystemInfo(itemPath);
- var modificationDate = info.LastWriteTimeUtc;
- var itemLastModifiedFileSystem = item.DateModified;
- if (info.Exists && itemLastModifiedFileSystem != modificationDate)
+ if (info.Exists && item.HasChanged(info.LastWriteTimeUtc))
{
- Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", itemLastModifiedFileSystem, modificationDate, itemPath);
+ Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, info.LastWriteTimeUtc, itemPath);
- item.DateModified = modificationDate;
+ item.DateModified = info.LastWriteTimeUtc;
if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
{
item.DateCreated = info.CreationTimeUtc;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 87ca728d3..43f0746ba 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -697,6 +697,7 @@ namespace MediaBrowser.Providers.Manager
{
_libraryMonitor.ReportFileSystemChangeBeginning(path);
await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
+ item.DateLastSaved = DateTime.UtcNow;
}
catch (Exception ex)
{
@@ -712,6 +713,7 @@ namespace MediaBrowser.Providers.Manager
try
{
await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
+ item.DateLastSaved = DateTime.UtcNow;
}
catch (Exception ex)
{
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 45e8553ea..c0680b901 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -192,7 +192,20 @@ namespace MediaBrowser.Providers.MediaInfo
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
- var albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSeparator);
+ string[]? albumArtists = null;
+ if (libraryOptions.PreferNonstandardArtistsTag)
+ {
+ TryGetSanitizedAdditionalFields(track, "ALBUMARTISTS", out var albumArtistsTagString);
+ if (albumArtistsTagString is not null)
+ {
+ albumArtists = albumArtistsTagString.Split(InternalValueSeparator);
+ }
+ }
+
+ if (albumArtists is null || albumArtists.Length == 0)
+ {
+ albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSeparator);
+ }
if (libraryOptions.UseCustomTagDelimiters)
{
@@ -205,7 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = albumArtist.Trim(),
+ Name = albumArtist,
Type = PersonKind.AlbumArtist
});
}
@@ -237,7 +250,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = performer.Trim(),
+ Name = performer,
Type = PersonKind.Artist
});
}
@@ -251,7 +264,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
- Name = composer.Trim(),
+ Name = composer,
Type = PersonKind.Composer
});
}
@@ -436,7 +449,11 @@ namespace MediaBrowser.Providers.MediaInfo
// Save extracted lyrics if they exist,
// and if the audio doesn't yet have lyrics.
- var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics;
+ // ATL supports both SRT and LRC formats as synchronized lyrics, but we only want to save LRC format.
+ var supportedLyrics = track.Lyrics.Where(l => l.Format != LyricsInfo.LyricsFormat.SRT).ToList();
+ var candidateSynchronizedLyric = supportedLyrics.FirstOrDefault(l => l.Format is not LyricsInfo.LyricsFormat.UNSYNCHRONIZED and not LyricsInfo.LyricsFormat.OTHER && l.SynchronizedLyrics is not null);
+ var candidateUnsynchronizedLyric = supportedLyrics.FirstOrDefault(l => l.Format is LyricsInfo.LyricsFormat.UNSYNCHRONIZED or LyricsInfo.LyricsFormat.OTHER && l.UnsynchronizedLyrics is not null);
+ var lyrics = candidateSynchronizedLyric is not null ? candidateSynchronizedLyric.FormatSynch() : candidateUnsynchronizedLyric?.UnsynchronizedLyrics;
if (!string.IsNullOrWhiteSpace(lyrics)
&& tryExtractEmbeddedLyrics)
{
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index d85f49b1d..bdb6b93be 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Chapters;
@@ -275,10 +276,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaStreamRepository.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
- if (mediaAttachments.Any())
- {
- _mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
- }
+ _mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh
|| options.MetadataRefreshMode == MetadataRefreshMode.Default)
@@ -516,12 +514,15 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var person in data.People)
{
- PeopleHelper.AddPerson(people, new PersonInfo
+ if (!string.IsNullOrWhiteSpace(person.Name))
{
- Name = person.Name.Trim(),
- Type = person.Type,
- Role = person.Role.Trim()
- });
+ PeopleHelper.AddPerson(people, new PersonInfo
+ {
+ Name = person.Name,
+ Type = person.Type,
+ Role = person.Role.Trim()
+ });
+ }
}
_libraryManager.UpdatePeople(video, people);
diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
index 8c673350d..bd6b36458 100644
--- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
@@ -130,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
- if (file is not null && file.LastWriteTimeUtc != item.DateModified && file.Length != item.Size)
+ if (file is not null && item.HasChanged(file.LastWriteTimeUtc) && file.Length != item.Size)
{
_logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path);
return true;
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index de2e79831..7c193b4d5 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -200,20 +200,26 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
foreach (var albumArtist in item.AlbumArtists)
{
- PeopleHelper.AddPerson(people, new PersonInfo
+ if (!string.IsNullOrWhiteSpace(albumArtist))
{
- Name = albumArtist.Trim(),
- Type = PersonKind.AlbumArtist
- });
+ PeopleHelper.AddPerson(people, new PersonInfo
+ {
+ Name = albumArtist,
+ Type = PersonKind.AlbumArtist
+ });
+ }
}
foreach (var artist in item.Artists)
{
- PeopleHelper.AddPerson(people, new PersonInfo
+ if (!string.IsNullOrWhiteSpace(artist))
{
- Name = artist.Trim(),
- Type = PersonKind.Artist
- });
+ PeopleHelper.AddPerson(people, new PersonInfo
+ {
+ Name = artist,
+ Type = PersonKind.Artist
+ });
+ }
}
LibraryManager.UpdatePeople(item, people);
diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
index a986b0b69..4c10fe3f1 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
@@ -215,7 +215,7 @@ public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
- if (file is not null && file.LastWriteTimeUtc != item.DateModified)
+ if (file is not null && item.HasChanged(file.LastWriteTimeUtc))
{
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
return true;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index a7c93ac4c..afbada3b3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
PersonKind.Producer
};
- [GeneratedRegex(@"[\W_]+")]
+ [GeneratedRegex(@"[\W_-[·]]+")]
private static partial Regex NonWordRegex();
/// <summary>
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index 768e4617b..926a962e2 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -56,7 +56,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
if (item.IsFileProtocol)
{
var file = directoryService.GetFile(item.Path);
- if (file is not null && item.DateModified != file.LastWriteTimeUtc)
+ if (file is not null && item.HasChanged(file.LastWriteTimeUtc))
{
return true;
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
index 6b35810b2..27dbeaba6 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Database.Implementations.DbConfiguration;
using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Database.Implementations;
@@ -20,7 +21,8 @@ public interface IJellyfinDatabaseProvider
/// Initialises jellyfins EFCore database access.
/// </summary>
/// <param name="options">The EFCore database options.</param>
- void Initialise(DbContextOptionsBuilder options);
+ /// <param name="databaseConfiguration">The Jellyfin database options.</param>
+ void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration);
/// <summary>
/// Will be invoked when EFCore wants to build its model.
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.Designer.cs
new file mode 100644
index 000000000..3ceb907c1
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.Designer.cs
@@ -0,0 +1,1709 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20250714044826_ResetJournalMode")]
+ partial class ResetJournalMode
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.7");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ IsFolder = false,
+ IsInMixedFolder = false,
+ IsLocked = false,
+ IsMovie = false,
+ IsRepeat = false,
+ IsSeries = false,
+ IsVirtualItem = false,
+ Name = "This is a placeholder item for UserData that has been detacted from its original item",
+ Type = "PLACEHOLDER"
+ });
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs
new file mode 100644
index 000000000..23cb0c8ba
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs
@@ -0,0 +1,22 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class ResetJournalMode : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ // Resets journal mode to WAL for users that have created their database during 10.11-RC1 or 2
+ migrationBuilder.Sql("PRAGMA journal_mode = 'WAL';", true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
index fe7870ed3..a7ff802af 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.7");
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
index dda1ca075..e52ab69d7 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
@@ -2,9 +2,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.DbConfiguration;
using MediaBrowser.Common.Configuration;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
@@ -38,11 +40,16 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
/// <inheritdoc/>
- public void Initialise(DbContextOptionsBuilder options)
+ public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration)
{
+ var sqliteConnectionBuilder = new SqliteConnectionStringBuilder();
+ sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
+ sqliteConnectionBuilder.Cache = Enum.Parse<SqliteCacheMode>(databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("cache", StringComparison.OrdinalIgnoreCase))?.Value ?? nameof(SqliteCacheMode.Default));
+ sqliteConnectionBuilder.Pooling = (databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("pooling", StringComparison.OrdinalIgnoreCase))?.Value ?? bool.FalseString).Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase);
+
options
.UseSqlite(
- $"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false",
+ sqliteConnectionBuilder.ToString(),
sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
// TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
.ConfigureWarnings(warnings =>
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 4626bc914..503e2f941 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -202,18 +202,47 @@ public class SkiaEncoder : IImageEncoder
}
}
- using var codec = SKCodec.Create(path, out SKCodecResult result);
+ var safePath = NormalizePath(path);
+ if (new FileInfo(safePath).Length == 0)
+ {
+ _logger.LogDebug("Skip zero‑byte image {FilePath}", path);
+ return default;
+ }
+
+ using var codec = SKCodec.Create(safePath, out var result);
+
switch (result)
{
case SKCodecResult.Success:
+ // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel
+ // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.)
+ // `SKCodec.Create` returns a *non‑null* codec together with
+ // SKCodecResult.InternalError. The header still contains valid dimensions,
+ // which is all we need here – so we fall back to them instead of aborting.
+ // See e.g. Skia bugs #4139, #6092.
+ case SKCodecResult.InternalError when codec is not null:
var info = codec.Info;
return new ImageDimensions(info.Width, info.Height);
+
case SKCodecResult.Unimplemented:
_logger.LogDebug("Image format not supported: {FilePath}", path);
return default;
+
default:
- _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
+ {
+ var boundsInfo = SKBitmap.DecodeBounds(safePath);
+
+ if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
+ {
+ return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
+ }
+
+ _logger.LogWarning(
+ "Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
+ path,
+ result);
return default;
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
index c04954207..be7ff5297 100644
--- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
+++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
@@ -73,6 +73,10 @@ namespace Jellyfin.LiveTv.IO
{
_targetPath = targetFile;
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+ if (!File.Exists(targetFile))
+ {
+ FileHelper.CreateEmpty(targetFile);
+ }
var processStartInfo = new ProcessStartInfo
{
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index 51eb99f49..6d887c577 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-trailer.mp4", ExtraType.Trailer);
Test("300.trailer.mp4", ExtraType.Trailer);
Test("300_trailer.mp4", ExtraType.Trailer);
- Test("300 trailer.mp4", ExtraType.Trailer);
+ Test("300 - trailer.mp4", ExtraType.Trailer);
Test("theme.mp3", ExtraType.ThemeSong);
}
@@ -132,7 +132,14 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-sample.mp4", ExtraType.Sample);
Test("300.sample.mp4", ExtraType.Sample);
Test("300_sample.mp4", ExtraType.Sample);
- Test("300 sample.mp4", ExtraType.Sample);
+ Test("300 - sample.mp4", ExtraType.Sample);
+ }
+
+ [Fact]
+ public void TestSuffixPartOfTitle()
+ {
+ Test("I Live In A Trailer.mp4", null);
+ Test("The DNA Sample.mp4", null);
}
private void Test(string input, ExtraType? expectedType)
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 377f82eac..d3164ba9c 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -87,7 +87,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"300.mkv",
- "300 trailer.mkv"
+ "300 - trailer.mkv"
};
var result = VideoListResolver.Resolve(
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/CoreResolutionIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/CoreResolutionIgnoreRuleTest.cs
new file mode 100644
index 000000000..0495c209d
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/CoreResolutionIgnoreRuleTest.cs
@@ -0,0 +1,129 @@
+using System;
+using System.IO;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Emby.Server.Implementations.Library;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class CoreResolutionIgnoreRuleTest
+{
+ private readonly CoreResolutionIgnoreRule _rule;
+ private readonly NamingOptions _namingOptions;
+ private readonly Mock<IServerApplicationPaths> _appPathsMock;
+
+ public CoreResolutionIgnoreRuleTest()
+ {
+ _namingOptions = new NamingOptions();
+
+ _namingOptions.AllExtrasTypesFolderNames.TryAdd("extras", ExtraType.Trailer);
+
+ _appPathsMock = new Mock<IServerApplicationPaths>();
+ _appPathsMock.SetupGet(x => x.RootFolderPath).Returns("/server/root");
+
+ _rule = new CoreResolutionIgnoreRule(_namingOptions, _appPathsMock.Object);
+ }
+
+ private FileSystemMetadata MakeFileSystemMetadata(string fullName, bool isDirectory = false)
+ => new FileSystemMetadata { FullName = fullName, Name = Path.GetFileName(fullName), IsDirectory = isDirectory };
+
+ private BaseItem MakeParent(string name = "Parent", bool isTopParent = false, Type? type = null)
+ {
+ return type switch
+ {
+ Type t when t == typeof(Folder) => CreateMock<Folder>(name, isTopParent).Object,
+ Type t when t == typeof(AggregateFolder) => CreateMock<AggregateFolder>(name, isTopParent).Object,
+ Type t when t == typeof(UserRootFolder) => CreateMock<UserRootFolder>(name, isTopParent).Object,
+ _ => CreateMock<BaseItem>(name, isTopParent).Object
+ };
+ }
+
+ private static Mock<T> CreateMock<T>(string name, bool isTopParent)
+ where T : BaseItem
+ {
+ var mock = new Mock<T>();
+ mock.SetupGet(p => p.Name).Returns(name);
+ mock.SetupGet(p => p.IsTopParent).Returns(isTopParent);
+ return mock;
+ }
+
+ [Fact]
+ public void TestApplicationFolder()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/server/root/extras", isDirectory: true),
+ null));
+
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/server/root/small.jpg"),
+ null));
+ }
+
+ [Fact]
+ public void TestTopLevelDirectory()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("Series/Extras", true),
+ MakeParent(type: typeof(AggregateFolder))));
+
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("Series/Extras/Extras", true),
+ MakeParent(isTopParent: true)));
+ }
+
+ [Fact]
+ public void TestIgnorePatterns()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Media/big.jpg"),
+ MakeParent()));
+
+ Assert.True(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Media/small.jpg"),
+ MakeParent()));
+ }
+
+ [Fact]
+ public void TestExtrasTypesFolderNames()
+ {
+ FileSystemMetadata fileSystemMetadata = MakeFileSystemMetadata("/Movies/Up/extras", true);
+
+ Assert.False(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent(type: typeof(AggregateFolder))));
+
+ Assert.False(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent(type: typeof(UserRootFolder))));
+
+ Assert.False(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ null));
+
+ Assert.True(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent()));
+
+ Assert.True(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent(type: typeof(Folder))));
+ }
+
+ [Fact]
+ public void TestThemeSong()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Movies/Up/intro.mp3"),
+ MakeParent()));
+
+ Assert.True(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Movies/Up/theme.mp3"),
+ MakeParent()));
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index a7a1e5e81..6d6bba4fc 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
- Assert.Equal(191, cultures.Count);
+ Assert.Equal(194, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
index 6964c920b..3d8ea15a3 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
@@ -301,7 +301,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
var versionInfo = fixture.Create<VersionInfo>();
versionInfo.Version = new Version(1, 0).ToString();
- versionInfo.Timestamp = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
+ versionInfo.Timestamp = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture);
var packageInfo = fixture.Create<PackageInfo>();
packageInfo.Versions = new[] { versionInfo };