diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 70bcd49737..0000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-version: 2
-updates:
-- package-ecosystem: nuget
- directory: "/"
- schedule:
- interval: weekly
- time: '12:00'
- open-pull-requests-limit: 10
-
-- package-ecosystem: github-actions
- directory: '/'
- schedule:
- interval: weekly
- time: '12:00'
- open-pull-requests-limit: 10
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 0000000000..5ca683876a
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "github>jellyfin/.github//renovate-presets/dotnet"
+ ]
+}
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index 20294843d5..0989df64b9 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -14,7 +14,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
+ uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
@@ -26,7 +26,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -35,7 +35,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
@@ -44,7 +44,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -58,7 +58,7 @@ jobs:
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
@@ -67,7 +67,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
- uses: alex-page/github-project-automation-plus@v0.8.1
+ uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 1dbd7fa367..adca9680fb 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
- name: Setup .NET Core
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
dotnet-version: '6.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@312e093a1892bd801f026f1090904ee8e460b9b6 # v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@312e093a1892bd801f026f1090904ee8e460b9b6 # v2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@312e093a1892bd801f026f1090904ee8e460b9b6 # v2
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 23873706d2..a29519b296 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -16,20 +16,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
- uses: cirrus-actions/rebase@1.7
+ uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
@@ -39,7 +39,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -47,14 +47,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -89,7 +89,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -104,7 +104,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index ceb4e8cdff..ca710fe834 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -12,18 +12,18 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET Core
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
dotnet-version: '6.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with:
name: openapi-head
retention-days: 14
@@ -37,17 +37,17 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
with:
ref: ${{ github.base_ref }}
- name: Setup .NET Core
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
dotnet-version: '6.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with:
name: openapi-base
retention-days: 14
@@ -63,12 +63,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
with:
name: openapi-base
path: openapi-base
@@ -90,14 +90,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@v2
+ uses: peter-evans/find-comment@b657a70ff16d17651703a84bee1cb9ad9d2be2ea # tag=v2
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -112,7 +112,7 @@ jobs:
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 2578f82cfe..f7a77f02b1 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@v6
+ - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
diff --git a/.gitignore b/.gitignore
index c2ae76c1e3..fd8e1f314b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -150,8 +150,6 @@ publish/
*.pubxml
# NuGet Packages Directory
-## TODO: If you have NuGet Package Restore enabled, uncomment the next line
-# packages/
dlls/
dllssigned/
@@ -166,7 +164,6 @@ AppPackages/
sql/
*.Cache
ClientBin/
-[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
diff --git a/Directory.Build.props b/Directory.Build.props
index efcfb72243..f812f44190 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,6 +16,7 @@
+
diff --git a/Dockerfile b/Dockerfile
index 219b958935..7b69a186ff 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -89,4 +89,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
- CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+ CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 8e0ba7af53..84ddf499a1 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -78,4 +78,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
- CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+ CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 790be1c39d..d4ae5802c0 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -72,4 +72,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--ffmpeg", "/usr/bin/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
- CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+ CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index d17e238715..68895a7fed 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -127,8 +127,7 @@ namespace Emby.Dlna.Eventing
public Task TriggerEvent(string notificationType, IDictionary stateVariables)
{
var subs = _subscriptions.Values
- .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase))
- .ToList();
+ .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase));
var tasks = subs.Select(i => TriggerEvent(i, stateVariables));
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index b73ce00b6f..65367e24f2 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -338,7 +338,6 @@ namespace Emby.Dlna.PlayTo
SubtitleStreamIndex = info.SubtitleStreamIndex,
VolumeLevel = _device.Volume,
- // TODO
CanSeek = true,
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index 2efe7d526f..6e491185d8 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -36,8 +36,7 @@ namespace Emby.Naming.AudioBook
// File with empty fullname will be sorted out here.
var audiobookFileInfos = files
.Select(i => _audioBookResolver.Resolve(i.FullName))
- .OfType()
- .ToList();
+ .OfType();
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 513733ab55..0119fa38c8 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -175,6 +175,7 @@ namespace Emby.Naming.Common
AlbumStackingPrefixes = new[]
{
"cd",
+ "digital media",
"disc",
"disk",
"vol",
@@ -512,13 +513,13 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
- ExtraType.Clip,
+ ExtraType.Short,
ExtraRuleType.DirectoryName,
"shorts",
MediaType.Video),
new ExtraRule(
- ExtraType.Clip,
+ ExtraType.Featurette,
ExtraRuleType.DirectoryName,
"featurettes",
MediaType.Video),
@@ -535,6 +536,12 @@ namespace Emby.Naming.Common
"other",
MediaType.Video),
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.DirectoryName,
+ "clips",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,
@@ -638,13 +645,13 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
- ExtraType.Clip,
+ ExtraType.Featurette,
ExtraRuleType.Suffix,
"-featurette",
MediaType.Video),
new ExtraRule(
- ExtraType.Clip,
+ ExtraType.Short,
ExtraRuleType.Suffix,
"-short",
MediaType.Video),
diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs
index 8b281e487f..ac90cc8ec5 100644
--- a/Emby.Notifications/NotificationManager.cs
+++ b/Emby.Notifications/NotificationManager.cs
@@ -88,8 +88,7 @@ namespace Emby.Notifications
string description,
CancellationToken cancellationToken)
{
- users = users.Where(i => IsEnabledForUser(service, i))
- .ToList();
+ users = users.Where(i => IsEnabledForUser(service, i));
var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken));
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 909972469e..8e4c13def0 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -48,6 +48,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -101,6 +102,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -652,6 +654,17 @@ namespace Emby.Server.Implementations
/// A task representing the service initialization operation.
public async Task InitializeServices()
{
+ var jellyfinDb = await Resolve>().CreateDbContextAsync().ConfigureAwait(false);
+ await using (jellyfinDb.ConfigureAwait(false))
+ {
+ if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
+ {
+ Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
+ await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
+ Logger.LogInformation("EFCore migrations applied successfully");
+ }
+ }
+
var localizationManager = (LocalizationManager)Resolve();
await localizationManager.LoadAll().ConfigureAwait(false);
@@ -1088,15 +1101,7 @@ namespace Emby.Server.Implementations
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
}
- // Published server ends with a /
- if (!string.IsNullOrEmpty(PublishedServerUrl))
- {
- // Published server ends with a '/', so we need to remove it.
- return PublishedServerUrl.Trim('/');
- }
-
- string smart = NetManager.GetBindInterface(request, out var port);
- return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
+ return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
///
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 5fc2e39a7a..187e0c9b3b 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -232,10 +232,10 @@ namespace Emby.Server.Implementations.Collections
if (list.Count > 0)
{
- var newList = collection.LinkedChildren.ToList();
- newList.AddRange(list);
- collection.LinkedChildren = newList.ToArray();
-
+ LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
+ collection.LinkedChildren.CopyTo(newChildren, 0);
+ list.CopyTo(newChildren, collection.LinkedChildren.Length);
+ collection.LinkedChildren = newChildren;
collection.UpdateRatingToItems(linkedChildrenList);
await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 7622d2fe68..371111dffe 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -3524,6 +3524,13 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
}
+ if (query.MinParentAndIndexNumber.HasValue)
+ {
+ whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
+ statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
+ statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
+ }
+
if (query.MinDateCreated.HasValue)
{
whereClauses.Add("DateCreated>=@MinDateCreated");
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index c4fdf08b9a..eeaa3346d9 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -25,13 +25,13 @@
-
+
-
-
-
+
+
+
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 9e35d83aa5..d5e4a636ef 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
}
- var collectionFolders = _libraryManager.GetCollectionFolders(item).ToList();
+ var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
{
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 657daac3fc..c1422c43da 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -79,14 +79,6 @@ namespace Emby.Server.Implementations.IO
TemporarilyIgnore(path);
}
- public bool IsPathLocked(string path)
- {
- // This method is not used by the core but it used by auto-organize
-
- var lockedPaths = _tempIgnoredPaths.Keys.ToList();
- return lockedPaths.Any(i => _fileSystem.AreEqual(i, path) || _fileSystem.ContainsSubPath(i, path));
- }
-
public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
{
if (string.IsNullOrEmpty(path))
@@ -145,8 +137,7 @@ namespace Emby.Server.Implementations.IO
.OfType()
.SelectMany(f => f.PhysicalLocations)
.Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(i => i)
- .ToList();
+ .OrderBy(i => i);
foreach (var path in paths)
{
@@ -372,11 +363,8 @@ namespace Emby.Server.Implementations.IO
var monitorPath = !IgnorePatterns.ShouldIgnore(path);
- // Ignore certain files
- var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
-
- // If the parent of an ignored path has a change event, ignore that too
- if (tempIgnorePaths.Any(i =>
+ // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
+ if (_tempIgnoredPaths.Keys.Any(i =>
{
if (_fileSystem.AreEqual(i, path))
{
@@ -491,7 +479,7 @@ namespace Emby.Server.Implementations.IO
{
lock (_activeRefreshers)
{
- foreach (var refresher in _activeRefreshers.ToList())
+ foreach (var refresher in _activeRefreshers)
{
refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index cef82ebbcc..b688af5286 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2590,9 +2590,9 @@ namespace Emby.Server.Implementations.Library
{
/*
Anime series don't generally have a season in their file name, however,
- tvdb needs a season to correctly get the metadata.
+ TVDb needs a season to correctly get the metadata.
Hence, a null season needs to be filled with something. */
- // FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified
+ // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
episode.ParentIndexNumber = 1;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 8f9e5f01b1..84d4688aff 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -376,7 +376,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!justName.IsEmpty)
{
- // check for tmdb id
+ // Check for TMDb id
var tmdbid = justName.GetAttributeValue("tmdbid");
if (!string.IsNullOrWhiteSpace(tmdbid))
@@ -387,7 +387,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!string.IsNullOrEmpty(item.Path))
{
- // check for imdb id - we use full media path, as we can assume, that this will match in any use case (either id in parent dir or in file name)
+ // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
if (!string.IsNullOrWhiteSpace(imdbid))
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 6b0dfe9864..7a2b3da3a9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -31,16 +31,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (args.IsDirectory)
{
// It's a boxset if the path is a directory with [playlist] in it's the name
- // TODO: Should this use Path.GetDirectoryName() instead?
- bool isBoxSet = Path.GetFileName(args.Path)
- ?.Contains("[playlist]", StringComparison.OrdinalIgnoreCase)
- ?? false;
- if (isBoxSet)
+ var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
+ if (string.IsNullOrEmpty(filename))
+ {
+ return null;
+ }
+
+ if (filename.Contains("[playlist]", StringComparison.OrdinalIgnoreCase))
{
return new Playlist
{
Path = args.Path,
- Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+ Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
};
}
@@ -51,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return new Playlist
{
Path = args.Path,
- Name = Path.GetFileName(args.Path)
+ Name = filename
};
}
}
@@ -60,8 +62,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// It should have the correct collection type and a supported file extension
else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
- var extension = Path.GetExtension(args.Path);
- if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ var extension = Path.GetExtension(args.Path.AsSpan());
+ if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return new Playlist
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 74321a256e..cf9be5a54a 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -2192,16 +2192,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void HandleDuplicateShowIds(List timers)
{
- foreach (var timer in timers.Skip(1))
+ // sort showings by HD channels first, then by startDate, record earliest showing possible
+ foreach (var timer in timers.OrderByDescending(t => _liveTvManager.GetLiveTvChannel(t, this).IsHD).ThenBy(t => t.StartDate).Skip(1))
{
- // TODO: Get smarter, prefer HD, etc
-
timer.Status = RecordingStatus.Cancelled;
_timerProvider.Update(timer);
}
}
- private void SearchForDuplicateShowIds(List timers)
+ private void SearchForDuplicateShowIds(IEnumerable timers)
{
var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList();
@@ -2282,39 +2281,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (updateTimerSettings)
{
- // Only update if not currently active - test both new timer and existing in case Id's are different
- // Id's could be different if the timer was created manually prior to series timer creation
- if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _))
- {
- UpdateExistingTimerWithNewMetadata(existingTimer, timer);
-
- // Needed by ShouldCancelTimerForSeriesTimer
- timer.IsManual = existingTimer.IsManual;
-
- if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
- {
- existingTimer.Status = RecordingStatus.Cancelled;
- }
- else if (!existingTimer.IsManual)
- {
- existingTimer.Status = RecordingStatus.New;
- }
-
- if (existingTimer.Status != RecordingStatus.Cancelled)
- {
- enabledTimersForSeries.Add(existingTimer);
- }
-
- existingTimer.KeepUntil = seriesTimer.KeepUntil;
- existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
- existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
- existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
- existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
- existingTimer.Priority = seriesTimer.Priority;
- existingTimer.SeriesTimerId = seriesTimer.Id;
-
- _timerProvider.Update(existingTimer);
- }
+ existingTimer.KeepUntil = seriesTimer.KeepUntil;
+ existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
+ existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
+ existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
+ existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
+ existingTimer.Priority = seriesTimer.Priority;
+ existingTimer.SeriesTimerId = seriesTimer.Id;
}
existingTimer.SeriesTimerId = seriesTimer.Id;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index a861e6ae44..f612565d1b 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -122,11 +122,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (_timers.TryAdd(item.Id, timer))
{
- Logger.LogInformation(
- "Creating recording timer for {Id}, {Name}. Timer will fire in {Minutes} minutes",
+ if (item.IsSeries)
+ {
+ Logger.LogInformation(
+ "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}",
item.Id,
item.Name,
- dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+ item.SeasonNumber,
+ item.EpisodeNumber,
+ item.ChannelId,
+ dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture),
+ item.StartDate);
+ }
+ else
+ {
+ Logger.LogInformation(
+ "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}",
+ item.Id,
+ item.Name,
+ item.ChannelId,
+ dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture),
+ item.StartDate);
+ }
}
else
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 4311db28d2..b981ad81a7 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -166,12 +166,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
const double DesiredAspect = 2.0 / 3;
- programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
- GetProgramImage(ApiUrl, allImages, DesiredAspect);
+ programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ??
+ GetProgramImage(ApiUrl, allImages, DesiredAspect, token);
const double WideAspect = 16.0 / 9;
- programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
+ programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token);
// Don't supply the same image twice
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
@@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programEntry.ThumbImage = null;
}
- programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
+ programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token);
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
@@ -400,7 +400,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return info;
}
- private static string GetProgramImage(string apiUrl, IEnumerable images, double desiredAspect)
+ private static string GetProgramImage(string apiUrl, IEnumerable images, double desiredAspect, string token)
{
var match = images
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
@@ -424,7 +424,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
else
{
- return apiUrl + "/image/" + uri;
+ return apiUrl + "/image/" + uri + "?token=" + token;
}
}
@@ -458,6 +458,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
IReadOnlyList programIds,
CancellationToken cancellationToken)
{
+ var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
+
if (programIds.Count == 0)
{
return Array.Empty();
@@ -479,6 +481,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
};
+ message.Headers.TryAddWithoutValidation("token", token);
try
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 7570a2bcf9..82f0baf32e 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -32,18 +32,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
public XmlTvListingsProvider(
IServerConfigurationManager config,
IHttpClientFactory httpClientFactory,
- ILogger logger,
- IFileSystem fileSystem)
+ ILogger logger)
{
_config = config;
_httpClientFactory = httpClientFactory;
_logger = logger;
- _fileSystem = fileSystem;
}
public string Name => "XmlTV";
@@ -165,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
CommunityRating = program.StarRating,
- SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+ SeriesId = program.Episode == null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
if (string.IsNullOrWhiteSpace(program.ProgramId))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 48d9e316de..e67b5846aa 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
+ return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none");
}
finally
{
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 9dc2fe7996..ada3c77301 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -97,7 +97,7 @@
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
- "TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
+ "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
"TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصل",
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 644d2676ee..ab04693ccf 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimitzar 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 durar molt de temps.",
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
- "External": "Extern"
+ "External": "Extern",
+ "HearingImpaired": "Discapacitat Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 943fc651f7..08db5a30e0 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimalizovat databázi",
"TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
"TaskKeyframeExtractor": "Vytahovač klíčových snímků",
- "External": "Externí"
+ "External": "Externí",
+ "HearingImpaired": "Sluchově postižení"
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 57455587d2..34655ace69 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimér database",
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan godt tage lang tid.",
"TaskKeyframeExtractor": "Billedramme udtrækker",
- "External": "Ekstern"
+ "External": "Ekstern",
+ "HearingImpaired": "Hørehæmmet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 9c278db4d2..e1c3e9de12 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Datenbank optimieren",
"TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor",
- "External": "Extern"
+ "External": "Extern",
+ "HearingImpaired": "Hörgeschädigt"
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 9e216a1660..8e9287af48 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων",
"TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
- "External": "Εξωτερικό"
+ "External": "Εξωτερικό",
+ "HearingImpaired": "Με προβλήματα ακοής"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 862410c54b..2436883883 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimise database",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskKeyframeExtractor": "Keyframe Extractor",
- "External": "External"
+ "External": "External",
+ "HearingImpaired": "Hearing Impaired"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 1289172bac..8ad9e8c716 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimización de base de datos",
"External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
- "TaskKeyframeExtractor": "Extractor de Fotogramas Clave"
+ "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
+ "HearingImpaired": "Personas con discapacidad auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index a7391cc882..d677cc46c7 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.",
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Discapacidad Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index db65a0c6d5..afffdf3bfa 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "Optimiza y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
"TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Discapacidad Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index da44e53d00..081462407d 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -120,5 +120,8 @@
"UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
"UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
- "External": "Väline"
+ "External": "Väline",
+ "HearingImpaired": "Kuulmispuudega",
+ "TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
+ "TaskKeyframeExtractor": "Võtmekaadri ekstraktor"
}
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index dfedce7b3a..d657ac7b69 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -116,5 +116,12 @@
"CameraImageUploadedFrom": "{0}-tik kamera irudi berri bat igo da",
"AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
"Application": "Aplikazioa",
- "AppDeviceValues": "App: {0}, Gailua: {1}"
+ "AppDeviceValues": "App: {0}, Gailua: {1}",
+ "HearingImpaired": "Entzunaldia aldatua",
+ "ProviderValue": "Hornitzailea: {0}",
+ "TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
+ "HeaderRecordingGroups": "Grabaketa taldeak",
+ "Inherit": "Oinordetu",
+ "TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
+ "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index f0cafd1c0d..ec72d58dd6 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimoi tietokanta",
"TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
"TaskKeyframeExtractor": "Avainkuvien purkain",
- "External": "Ulkoinen"
+ "External": "Ulkoinen",
+ "HearingImpaired": "Kuulorajoitteinen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 24ca8f8611..3ee045d89e 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
+ "CameraImageUploadedFrom": "Une nouvelle photo a été téléversée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
"TaskKeyframeExtractor": "Extracteur d'image clé",
- "External": "Externe"
+ "External": "Externe",
+ "HearingImpaired": "Malentendants"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 648c878e9c..768245a09b 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
"TaskKeyframeExtractor": "Extracteur d'image clé",
- "External": "Externe"
+ "External": "Externe",
+ "HearingImpaired": "Malentendants"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index b433c6f68c..76a98aa54b 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -47,7 +47,7 @@
"HeaderFavoriteEpisodes": "Episodios Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteAlbums": "Álbunes Favoritos",
- "HeaderContinueWatching": "Seguir mirando",
+ "HeaderContinueWatching": "Seguir vendo",
"HeaderAlbumArtists": "Artistas do Album",
"Genres": "Xéneros",
"Forced": "Forzado",
@@ -119,5 +119,9 @@
"UserOnlineFromDevice": "{0} está en liña desde {1}",
"UserOfflineFromDevice": "{0} desconectouse desde {1}",
"TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
- "TaskOptimizeDatabase": "Optimizar base de datos"
+ "TaskOptimizeDatabase": "Optimizar base de datos",
+ "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
+ "External": "Externo",
+ "HearingImpaired": "Problemas de audición",
+ "TaskKeyframeExtractor": "Extractor de fragmentos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index c635dab23a..694a3d688c 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים ומוריד את שטח האחסון שבשימוש. הרצה של פעולה זו לאחר סריקת הספרייה או שינויים אחרים שמשפיעים על מסד הנתונים יכולה לשפר ביצועים.",
"TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.",
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
- "External": "חיצוני"
+ "External": "חיצוני",
+ "HearingImpaired": "לקוי שמיעה"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index c63cd2b94f..d01295419e 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -123,5 +123,6 @@
"External": "Vanjski",
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
- "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka."
+ "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
+ "HearingImpaired": "Oštećen sluh"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index c7f2f9c85e..62d48cebd8 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Adatbázis optimalizálása",
"TaskKeyframeExtractor": "Kulcskockák kibontása",
"TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
- "External": "Külső"
+ "External": "Külső",
+ "HearingImpaired": "Hallássérült"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 3e05525c87..695c0f4048 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimalkan basis data",
"TaskKeyframeExtractorDescription": "Ekstrak bingkai utama dari file video untuk membuat daftar putar HLS yang lebih tepat. Tugas ini dapat berjalan untuk waktu yang lama.",
"TaskKeyframeExtractor": "Ekstraktor Bingkai Utama",
- "External": "Luar"
+ "External": "Luar",
+ "HearingImpaired": "Gangguan Pendengaran"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 2aa84c536f..3710f03e07 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Ottimizza Database",
"TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
- "External": "Esterno"
+ "External": "Esterno",
+ "HearingImpaired": "con problemi di udito"
}
diff --git a/Emby.Server.Implementations/Localization/Core/jbo.json b/Emby.Server.Implementations/Localization/Core/jbo.json
new file mode 100644
index 0000000000..1b47bb2f23
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/jbo.json
@@ -0,0 +1,7 @@
+{
+ "Albums": "lo albuma",
+ "Artists": "lo larpra",
+ "Books": "lo cukta",
+ "HeaderAlbumArtists": "lo albuma larpra",
+ "Playlists": "lo zgipor"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json
new file mode 100644
index 0000000000..02f9d44436
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/km.json
@@ -0,0 +1,3 @@
+{
+ "Albums": "Albums"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 232b3ec934..e1c937b6cd 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -123,5 +123,6 @@
"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štraukė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"
+ "External": "Išorinis",
+ "HearingImpaired": "Su klausos sutrikimais"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 77ee46a4f7..5c7dec7efe 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "Komprimerer database og frigjør plass. Denne prosessen kan forbedre ytelsen etter skanning av bibliotek eller andre handlinger som fører til databaseendringer.",
"TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.",
"TaskKeyframeExtractor": "Nøkkelbilde-uttrekker",
- "External": "Ekstern"
+ "External": "Ekstern",
+ "HearingImpaired": "Hørselshemmet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 3f22355d6a..c05114f013 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -5,7 +5,7 @@
"Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
"Books": "Boeken",
- "CameraImageUploadedFrom": "Nieuwe camera afbeelding toegevoegd vanaf {0}",
+ "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
"Collections": "Verzamelingen",
@@ -15,7 +15,7 @@
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
- "HeaderAlbumArtists": "Album Artiesten",
+ "HeaderAlbumArtists": "Albumartiesten",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Database optimaliseren",
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS afspeellijsten te maken. Dit kan lang duren.",
"TaskKeyframeExtractor": "Keyframe Extractor",
- "External": "Extern"
+ "External": "Extern",
+ "HearingImpaired": "Slechthorend"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 38a36a7e01..b9b93b7b6f 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de arquivos de vídeo para criar listas de reprodução HLS mais precisas. Esta tarefa pode ser executada por um longo tempo.",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Deficiência Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 7047f1c282..a75182f220 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Surdo"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index c2c77ccab1..39229f45f9 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -120,5 +120,6 @@
"TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Problemas auditivos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 53456269af..2c10bb4779 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -11,7 +11,7 @@
"UserOfflineFromDevice": "{0} s-a deconectat de la {1}",
"UserLockedOutWithName": "Utilizatorul {0} a fost blocat",
"UserDownloadingItemWithValues": "{0} descarcă {1}",
- "UserDeletedWithName": "Utilizatorul {0} a fost eliminat",
+ "UserDeletedWithName": "Utilizatorul {0} a fost șters",
"UserCreatedWithName": "Utilizatorul {0} a fost creat",
"User": "Utilizator",
"TvShows": "Seriale TV",
@@ -20,33 +20,33 @@
"SubtitleDownloadFailureFromForItem": "Subtitrările nu au putut fi descărcate de la {0} pentru {1}",
"StartupEmbyServerIsLoading": "Se încarcă serverul Jellyfin. Încercați din nou în scurt timp.",
"Songs": "Melodii",
- "Shows": "Spectacole",
- "ServerNameNeedsToBeRestarted": "{0} trebuie repornit",
+ "Shows": "Seriale",
+ "ServerNameNeedsToBeRestarted": "{0} trebuie să fie repornit",
"ScheduledTaskStartedWithName": "{0} pornit/ă",
"ScheduledTaskFailedWithName": "{0} eșuat/ă",
"ProviderValue": "Furnizor: {0}",
"PluginUpdatedWithName": "{0} a fost actualizat/ă",
"PluginUninstalledWithName": "{0} a fost dezinstalat",
"PluginInstalledWithName": "{0} a fost instalat",
- "Plugin": "Plugin",
- "Playlists": "Liste redare",
+ "Plugin": "Extensie",
+ "Playlists": "Liste de redare",
"Photos": "Fotografii",
"NotificationOptionVideoPlaybackStopped": "Redarea video oprită",
"NotificationOptionVideoPlayback": "Redare video începută",
"NotificationOptionUserLockedOut": "Utilizatorul a fost blocat",
- "NotificationOptionTaskFailed": "Activitate programata eșuată",
+ "NotificationOptionTaskFailed": "Activitate programată eșuată",
"NotificationOptionServerRestartRequired": "Este necesară repornirea serverului",
- "NotificationOptionPluginUpdateInstalled": "Actualizare plugin instalată",
- "NotificationOptionPluginUninstalled": "Plugin dezinstalat",
- "NotificationOptionPluginInstalled": "Plugin instalat",
- "NotificationOptionPluginError": "Plugin-ul a eșuat",
- "NotificationOptionNewLibraryContent": "Adăugat conținut nou",
- "NotificationOptionInstallationFailed": "Eșec la instalare",
- "NotificationOptionCameraImageUploaded": "Încarcată imagine cameră",
+ "NotificationOptionPluginUpdateInstalled": "Actualizarea extensiei este instalată",
+ "NotificationOptionPluginUninstalled": "Extensie dezinstalată",
+ "NotificationOptionPluginInstalled": "Extensie instalată",
+ "NotificationOptionPluginError": "Eroare de extensie",
+ "NotificationOptionNewLibraryContent": "A fost adăugat conținut nou",
+ "NotificationOptionInstallationFailed": "Instalare eșuată",
+ "NotificationOptionCameraImageUploaded": "Imagine încarcată",
"NotificationOptionAudioPlaybackStopped": "Redare audio oprită",
"NotificationOptionAudioPlayback": "A început redarea audio",
"NotificationOptionApplicationUpdateInstalled": "Actualizarea aplicației a fost instalată",
- "NotificationOptionApplicationUpdateAvailable": "Disponibilă o actualizare a aplicației",
+ "NotificationOptionApplicationUpdateAvailable": "Este disponibilă o actualizare a aplicației",
"NewVersionIsAvailable": "O nouă versiune a Jellyfin Server este disponibilă pentru descărcare.",
"NameSeasonUnknown": "Sezon Necunoscut",
"NameSeasonNumber": "Sezonul {0}",
@@ -54,8 +54,8 @@
"MusicVideos": "Videoclipuri muzicale",
"Music": "Muzică",
"Movies": "Filme",
- "MixedContent": "Conținut mixt",
- "MessageServerConfigurationUpdated": "Configurația serverului a fost actualizată",
+ "MixedContent": "Conținut amestecat",
+ "MessageServerConfigurationUpdated": "Configurarea serverului a fost actualizată",
"MessageNamedServerConfigurationUpdatedWithValue": "Secțiunea de configurare a serverului {0} a fost acualizata",
"MessageApplicationUpdatedTo": "Jellyfin Server a fost actualizat la {0}",
"MessageApplicationUpdated": "Jellyfin Server a fost actualizat",
@@ -69,7 +69,7 @@
"HeaderRecordingGroups": "Grupuri de înregistrare",
"HeaderLiveTV": "TV în Direct",
"HeaderFavoriteSongs": "Melodii Favorite",
- "HeaderFavoriteShows": "Spectacole Favorite",
+ "HeaderFavoriteShows": "Seriale TV Favorite",
"HeaderFavoriteEpisodes": "Episoade Favorite",
"HeaderFavoriteArtists": "Artiști Favoriți",
"HeaderFavoriteAlbums": "Albume Favorite",
@@ -97,10 +97,10 @@
"TaskRefreshChannels": "Actualizează canale",
"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 pluginuri care sunt configurate să se actualizeze automat.",
- "TaskUpdatePlugins": "Actualizați plugin-uri",
+ "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.",
+ "TaskUpdatePlugins": "Actualizați Extensile",
"TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.",
- "TaskRefreshPeople": "Actualizează oamenii",
+ "TaskRefreshPeople": "Actualizează Persoanele",
"TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.",
"TaskCleanLogs": "Curățare director jurnal",
"TaskRefreshLibraryDescription": "Scanează biblioteca media pentru fișiere noi și reîmprospătează metadatele.",
@@ -114,13 +114,14 @@
"TasksLibraryCategory": "Librărie",
"TasksMaintenanceCategory": "Mentenanță",
"TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.",
- "TaskCleanActivityLog": "Curăță Jurnalul de Activitate",
+ "TaskCleanActivityLog": "Curăță Jurnalul de Activități",
"Undefined": "Nedefinit",
"Forced": "Forțat",
"Default": "Implicit",
- "TaskOptimizeDatabaseDescription": "Compactează baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.",
+ "TaskOptimizeDatabaseDescription": "Comprimă baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.",
"TaskOptimizeDatabase": "Optimizează baza de date",
"TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.",
"External": "Extern",
- "TaskKeyframeExtractor": "Extractor de cadre cheie"
+ "TaskKeyframeExtractor": "Extractor de cadre cheie",
+ "HearingImpaired": "Ascultare Impară"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index ea9a82d2bf..dc45a8264d 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -75,7 +75,7 @@
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
- "Sync": "Синхро",
+ "Sync": "Синхронизация",
"System": "Система",
"TvShows": "ТВ",
"User": "Пользователь",
@@ -117,11 +117,12 @@
"TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
"TaskCleanActivityLog": "Очистка журнала активности",
"Undefined": "Не определено",
- "Forced": "Форсир-ые",
+ "Forced": "Принудительно",
"Default": "По умолчанию",
"TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
"TaskOptimizeDatabase": "Оптимизация базы данных",
"TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
"TaskKeyframeExtractor": "Извлечение ключевых кадров",
- "External": "Внешние"
+ "External": "Внешние",
+ "HearingImpaired": "Для слабослышащих"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 7502969a64..858cc40dd8 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimalizovať databázu",
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
- "External": "Externé"
+ "External": "Externé",
+ "HearingImpaired": "Sluchovo Postihnutý"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index 2766dab06a..d1b73a3eb9 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -119,5 +119,9 @@
"Forced": "I detyruar",
"Default": "Parazgjedhur",
"TaskOptimizeDatabaseDescription": "Kompakton bazën e të dhënave dhe shkurton hapësirën e lirë. Drejtimi i kësaj detyre pasi skanoni bibliotekën ose bëni ndryshime të tjera që nënkuptojnë modifikime të bazës së të dhënave mund të përmirësojë performancën.",
- "TaskOptimizeDatabase": "Optimizo databazën"
+ "TaskOptimizeDatabase": "Optimizo databazën",
+ "TaskKeyframeExtractorDescription": "Nxjerrë kornizat kryesore nga skedarët video për të krijuar lista luajtjeje më të sakta HLS. Ky veprim mund të dojë një kohë të gjatë për tu kompletuar.",
+ "TaskKeyframeExtractor": "Nxjerrës i kornizës kryesore",
+ "External": "Jashtem",
+ "HearingImpaired": "Dëgjimi i dëmtuar"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 3e0fd11c8e..92ce616f2e 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabaseDescription": "Стискає базу даних та збільшує вільний простір. Виконання цього завдання після сканування медіатеки або внесення інших змін, які передбачають модифікацію бази даних може покращити продуктивність.",
"TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.",
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
- "External": "Зовнішній"
+ "External": "Зовнішній",
+ "HearingImpaired": "З порушеннями слуху"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index e7f3e492c6..5413346d41 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -5,18 +5,18 @@
"HeaderAlbumArtists": "البم کے فنکار",
"Movies": "فلمیں",
"HeaderFavoriteEpisodes": "پسندیدہ اقساط",
- "Collections": "مجموعہ",
+ "Collections": "مجموعے",
"Folders": "فولڈرز",
"HeaderLiveTV": "براہ راست ٹی وی",
"Channels": "چینلز",
"HeaderContinueWatching": "دیکھنا جاری رکھیں",
"Playlists": "پلے لسٹس",
- "ValueSpecialEpisodeName": "خاص - {0}",
- "Shows": "شوز",
+ "ValueSpecialEpisodeName": "خصوصی - {0}",
+ "Shows": "دکھاتا ہے۔",
"Genres": "انواع",
"Artists": "فنکار",
- "Sync": "مطابقت",
- "Photos": "تصوریں",
+ "Sync": "مطابقت پذیری",
+ "Photos": "تصاویر",
"Albums": "البمز",
"Favorites": "پسندیدہ",
"Songs": "گانے",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index b9e2f1e6c2..44ce4ac5b2 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu",
"TaskKeyframeExtractor": "Trích Xuất Khung Hình",
"TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.",
- "External": "Bên ngoài"
+ "External": "Bên ngoài",
+ "HearingImpaired": "Khiếm Thính"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index a121fc376d..ccfbeef0ce 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "优化数据库",
"TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
"TaskKeyframeExtractor": "关键帧提取器",
- "External": "外部"
+ "External": "外部",
+ "HearingImpaired": "听力障碍"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 6c8bf76274..baa9ecc1c1 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -123,5 +123,6 @@
"TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。",
"TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。",
"TaskKeyframeExtractor": "關鍵幀提取器",
- "External": "外部"
+ "External": "外部",
+ "HearingImpaired": "聽力障礙"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 102a266f8e..4949c5ab6d 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -37,7 +37,7 @@
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
- "MusicVideos": "音樂錄影帶",
+ "MusicVideos": "MV",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "最佳化資料庫",
"TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。",
"TaskKeyframeExtractor": "關鍵幀提取器",
- "External": "外部"
+ "External": "外部",
+ "HearingImpaired": "聽力障礙"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 281dbb00b9..b771681266 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -386,6 +386,7 @@ namespace Emby.Server.Implementations.Localization
yield return new LocalizationOption("Español (Dominicana)", "es_DO");
yield return new LocalizationOption("Español (México)", "es-MX");
yield return new LocalizationOption("Eesti", "et");
+ yield return new LocalizationOption("Basque", "eu");
yield return new LocalizationOption("فارسی", "fa");
yield return new LocalizationOption("Suomi", "fi");
yield return new LocalizationOption("Filipino", "fil");
@@ -433,8 +434,8 @@ namespace Emby.Server.Implementations.Localization
yield return new LocalizationOption("Українська", "uk");
yield return new LocalizationOption("اُردُو", "ur_PK");
yield return new LocalizationOption("Tiếng Việt", "vi");
- yield return new LocalizationOption("汉语 (简化字)", "zh-CN");
- yield return new LocalizationOption("漢語 (繁体字)", "zh-TW");
+ yield return new LocalizationOption("汉语 (简体字)", "zh-CN");
+ yield return new LocalizationOption("漢語 (繁體字)", "zh-TW");
yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
}
}
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index ec4e0dbeb9..3f7d46822e 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -715,6 +715,7 @@ namespace Emby.Server.Implementations.Plugins
{
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
+ plugin.Manifest.AutoUpdate = false;
return;
}
@@ -729,6 +730,7 @@ namespace Emby.Server.Implementations.Plugins
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
+ plugin.Manifest.AutoUpdate = false;
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 0bf0838fac..da7c8732a8 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -16,6 +16,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
@@ -24,15 +25,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
///
public class ChapterImagesTask : IScheduledTask
{
- ///
- /// The _library manager.
- ///
+ private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
-
private readonly IItemRepository _itemRepo;
-
private readonly IApplicationPaths _appPaths;
-
private readonly IEncodingManager _encodingManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
@@ -40,6 +36,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
///
/// Initializes a new instance of the class.
///
+ /// The logger..
/// The library manager..
/// The item repository.
/// The application paths.
@@ -47,6 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// The filesystem.
/// The localization manager.
public ChapterImagesTask(
+ ILogger logger,
ILibraryManager libraryManager,
IItemRepository itemRepo,
IApplicationPaths appPaths,
@@ -54,6 +52,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
IFileSystem fileSystem,
ILocalizationManager localization)
{
+ _logger = logger;
_libraryManager = libraryManager;
_itemRepo = itemRepo;
_appPaths = appPaths;
@@ -167,9 +166,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
progress.Report(100 * percent);
}
- catch (ObjectDisposedException)
+ catch (ObjectDisposedException ex)
{
// TODO Investigate and properly fix.
+ _logger.LogError(ex, "Object Disposed");
break;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 98e45fa460..1efacd8562 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
- private readonly JellyfinDbProvider _provider;
+ private readonly IDbContextFactory _provider;
///
/// Initializes a new instance of the class.
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public OptimizeDatabaseTask(
ILogger logger,
ILocalizationManager localization,
- JellyfinDbProvider provider)
+ IDbContextFactory provider)
{
_logger = logger;
_localization = localization;
@@ -70,30 +70,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
///
- public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
{
_logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
try
{
- using var context = _provider.CreateContext();
- if (context.Database.IsSqlite())
+ var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
{
- context.Database.ExecuteSqlRaw("PRAGMA optimize");
- context.Database.ExecuteSqlRaw("VACUUM");
- _logger.LogInformation("jellyfin.db optimized successfully!");
- }
- else
- {
- _logger.LogInformation("This database doesn't support optimization");
+ if (context.Database.IsSqlite())
+ {
+ await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
+ await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("jellyfin.db optimized successfully!");
+ }
+ else
+ {
+ _logger.LogInformation("This database doesn't support optimization");
+ }
}
}
catch (Exception e)
{
_logger.LogError(e, "Error while optimizing jellyfin.db");
}
-
- return Task.CompletedTask;
}
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 6005896ad9..5c9b9df153 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -192,7 +192,6 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) },
IsPlayed = true,
Limit = 1,
ParentIndexNumberNotEquals = 0,
@@ -203,11 +202,10 @@ namespace Emby.Server.Implementations.TV
}
};
- if (rewatching)
- {
- // find last watched by date played, not by newest episode watched
- lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
- }
+ // If rewatching is enabled, sort first by date played and then by season and episode numbers
+ lastQuery.OrderBy = rewatching
+ ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
+ : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast().FirstOrDefault();
@@ -226,18 +224,16 @@ namespace Emby.Server.Implementations.TV
DtoOptions = dtoOptions
};
- Episode nextEpisode;
- if (rewatching)
+ // Locate the next up episode based on the last watched episode's season and episode number
+ var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
+ var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
+ if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
{
- nextQuery.Limit = 2;
- // get watched episode after most recently watched
- nextEpisode = _libraryManager.GetItemList(nextQuery).Cast().ElementAtOrDefault(1);
- }
- else
- {
- nextEpisode = _libraryManager.GetItemList(nextQuery).Cast().FirstOrDefault();
+ nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
}
+ var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast().FirstOrDefault();
+
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
{
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 64ee5680ce..14fd7eb3c1 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -178,7 +178,7 @@ namespace Jellyfin.Api.Controllers
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
{
- var order = int.Parse(key.AsSpan().Slice("homesection".Length));
+ var order = int.Parse(key.AsSpan().Slice("homesection".Length), NumberStyles.Any, CultureInfo.InvariantCulture);
if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out var type))
{
type = order < 8 ? defaults[order] : HomeSectionType.None;
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 80ae5abcbf..3ee5b8d737 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -87,9 +87,9 @@ namespace Jellyfin.Api.Controllers
/// Optional. The minimum last saved date for the current user. Format = ISO.
/// Optional. The maximum premiere date. Format = ISO.
/// Optional filter by items that have an overview or not.
- /// Optional filter by items that have an imdb id or not.
- /// Optional filter by items that have a tmdb id or not.
- /// Optional filter by items that have a tvdb id or not.
+ /// Optional filter by items that have an IMDb id or not.
+ /// Optional filter by items that have a TMDb id or not.
+ /// Optional filter by items that have a TVDb id or not.
/// Optional filter for live tv movies.
/// Optional filter for live tv series.
/// Optional filter for live tv news.
@@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The maximum number of records to return.
/// When searching within folders, this determines whether or not the search will be recursive. true/false.
/// Optional. Filter based on a search term.
- /// Sort Order - Ascending,Descending.
+ /// Sort Order - Ascending, Descending.
/// Specify this to localize the search to a specific item or folder. Omit to use the root.
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
@@ -282,39 +282,13 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { BaseItemKind.Playlist };
}
- var enabledChannels = isApiKey
- ? Array.Empty()
- : user!.GetPreferenceValues(PreferenceKind.EnabledChannels);
-
- // api keys are always enabled for all folders
- bool isInEnabledFolder = isApiKey
- || Array.IndexOf(user!.GetPreferenceValues(PreferenceKind.EnabledFolders), item.Id) != -1
- // Assume all folders inside an EnabledChannel are enabled
- || Array.IndexOf(enabledChannels, item.Id) != -1
- // Assume all items inside an EnabledChannel are enabled
- || Array.IndexOf(enabledChannels, item.ChannelId) != -1;
-
- if (!isInEnabledFolder)
- {
- var collectionFolders = _libraryManager.GetCollectionFolders(item);
- foreach (var collectionFolder in collectionFolders)
- {
- // api keys never enter this block, so user is never null
- if (user!.GetPreferenceValues(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
- {
- isInEnabledFolder = true;
- }
- }
- }
-
- // api keys are always enabled for all folders, so user is never null
if (item is not UserRootFolder
- && !isInEnabledFolder
- && !user!.HasPermission(PermissionKind.EnableAllFolders)
- && !user.HasPermission(PermissionKind.EnableAllChannels)
- && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
+ // api keys can always access all folders
+ && !isApiKey
+ // check the item is visible for the user
+ && !item.IsVisible(user))
{
- _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user.Username, item.Name);
+ _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
}
@@ -562,9 +536,9 @@ namespace Jellyfin.Api.Controllers
/// Optional. The minimum last saved date for the current user. Format = ISO.
/// Optional. The maximum premiere date. Format = ISO.
/// Optional filter by items that have an overview or not.
- /// Optional filter by items that have an imdb id or not.
- /// Optional filter by items that have a tmdb id or not.
- /// Optional filter by items that have a tvdb id or not.
+ /// Optional filter by items that have an IMDb id or not.
+ /// Optional filter by items that have a TMDb id or not.
+ /// Optional filter by items that have a TVDb id or not.
/// Optional filter for live tv movies.
/// Optional filter for live tv series.
/// Optional filter for live tv news.
@@ -575,7 +549,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The maximum number of records to return.
/// When searching within folders, this determines whether or not the search will be recursive. true/false.
/// Optional. Filter based on a search term.
- /// Sort Order - Ascending,Descending.
+ /// Sort Order - Ascending, Descending.
/// Specify this to localize the search to a specific item or folder. Omit to use the root.
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index e9492a6a47..7a57bf1a21 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -485,7 +485,7 @@ namespace Jellyfin.Api.Controllers
/// Media folders returned.
/// List of user media folders.
[HttpGet("Library/MediaFolders")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetMediaFolders([FromQuery] bool? isHidden)
{
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 8195fc7609..03f864b4a7 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
new InternalItemsQuery(user)
{
Person = name,
- // Account for duplicates by imdb id, since the database doesn't support this yet
+ // Account for duplicates by IMDb id, since the database doesn't support this yet
Limit = itemLimit + 2,
PersonTypes = new[] { PersonType.Director },
IncludeItemTypes = itemTypes.ToArray(),
@@ -232,15 +232,15 @@ namespace Jellyfin.Api.Controllers
foreach (var name in names)
{
var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Person = name,
- // Account for duplicates by imdb id, since the database doesn't support this yet
- Limit = itemLimit + 2,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+ {
+ Person = name,
+ // Account for duplicates by IMDb id, since the database doesn't support this yet
+ Limit = itemLimit + 2,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
.Select(x => x.First())
.Take(itemLimit)
.ToList();
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index b296d1c960..53a839e431 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -55,9 +55,9 @@ namespace Jellyfin.Api.Controllers
/// Optional. The minimum last saved date for the current user. Format = ISO.
/// Optional. The maximum premiere date. Format = ISO.
/// Optional filter by items that have an overview or not.
- /// Optional filter by items that have an imdb id or not.
- /// Optional filter by items that have a tmdb id or not.
- /// Optional filter by items that have a tvdb id or not.
+ /// Optional filter by items that have an IMDb id or not.
+ /// Optional filter by items that have a TMDb id or not.
+ /// Optional filter by items that have a TVDb id or not.
/// Optional filter for live tv movies.
/// Optional filter for live tv series.
/// Optional filter for live tv news.
@@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The maximum number of records to return.
/// When searching within folders, this determines whether or not the search will be recursive. true/false.
/// Optional. Filter based on a search term.
- /// Sort Order - Ascending,Descending.
+ /// Sort Order - Ascending, Descending.
/// Specify this to localize the search to a specific item or folder. Omit to use the root.
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 3705737739..c8e62999c4 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -351,7 +351,7 @@ namespace Jellyfin.Api.Helpers
try
{
// Parses npt times in the format of '10:19:25.7'
- return TimeSpan.Parse(value).Ticks;
+ return TimeSpan.Parse(value, CultureInfo.InvariantCulture).Ticks;
}
catch
{
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 7e64cf645b..a4502b6129 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 592c53fe55..9d6ca6aabe 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity
///
public class ActivityManager : IActivityManager
{
- private readonly JellyfinDbProvider _provider;
+ private readonly IDbContextFactory _provider;
///
/// Initializes a new instance of the class.
///
/// The Jellyfin database provider.
- public ActivityManager(JellyfinDbProvider provider)
+ public ActivityManager(IDbContextFactory provider)
{
_provider = provider;
}
@@ -32,10 +32,12 @@ namespace Jellyfin.Server.Implementations.Activity
///
public async Task CreateAsync(ActivityLog entry)
{
- await using var dbContext = _provider.CreateContext();
-
- dbContext.ActivityLogs.Add(entry);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.ActivityLogs.Add(entry);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
EntryCreated?.Invoke(this, new GenericEventArgs(ConvertToOldModel(entry)));
}
@@ -43,44 +45,47 @@ namespace Jellyfin.Server.Implementations.Activity
///
public async Task> GetPagedResultAsync(ActivityLogQuery query)
{
- await using var dbContext = _provider.CreateContext();
-
- IQueryable entries = dbContext.ActivityLogs
- .AsQueryable()
- .OrderByDescending(entry => entry.DateCreated);
-
- if (query.MinDate.HasValue)
+ var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
- }
+ IQueryable entries = dbContext.ActivityLogs
+ .OrderByDescending(entry => entry.DateCreated);
- if (query.HasUserId.HasValue)
- {
- entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
- }
+ if (query.MinDate.HasValue)
+ {
+ entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
+ }
- return new QueryResult(
- query.Skip,
- await entries.CountAsync().ConfigureAwait(false),
- await entries
- .Skip(query.Skip ?? 0)
- .Take(query.Limit ?? 100)
- .AsAsyncEnumerable()
- .Select(ConvertToOldModel)
- .ToListAsync()
- .ConfigureAwait(false));
+ if (query.HasUserId.HasValue)
+ {
+ entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
+ }
+
+ return new QueryResult(
+ query.Skip,
+ await entries.CountAsync().ConfigureAwait(false),
+ await entries
+ .Skip(query.Skip ?? 0)
+ .Take(query.Limit ?? 100)
+ .AsAsyncEnumerable()
+ .Select(ConvertToOldModel)
+ .ToListAsync()
+ .ConfigureAwait(false));
+ }
}
///
public async Task CleanAsync(DateTime startDate)
{
- await using var dbContext = _provider.CreateContext();
- var entries = dbContext.ActivityLogs
- .AsQueryable()
- .Where(entry => entry.DateCreated <= startDate);
+ var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var entries = dbContext.ActivityLogs
+ .Where(entry => entry.DateCreated <= startDate);
- dbContext.RemoveRange(entries);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ dbContext.RemoveRange(entries);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
}
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index 0728f11799..eeb958c620 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -22,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices
///
public class DeviceManager : IDeviceManager
{
- private readonly JellyfinDbProvider _dbProvider;
+ private readonly IDbContextFactory _dbProvider;
private readonly IUserManager _userManager;
private readonly ConcurrentDictionary _capabilitiesMap = new();
@@ -31,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices
///
/// The database provider.
/// The user manager.
- public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager)
+ public DeviceManager(IDbContextFactory dbProvider, IUserManager userManager)
{
_dbProvider = dbProvider;
_userManager = userManager;
@@ -49,16 +50,20 @@ namespace Jellyfin.Server.Implementations.Devices
///
public async Task UpdateDeviceOptions(string deviceId, string deviceName)
{
- await using var dbContext = _dbProvider.CreateContext();
- var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
- if (deviceOptions == null)
+ DeviceOptions? deviceOptions;
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- deviceOptions = new DeviceOptions(deviceId);
- dbContext.DeviceOptions.Add(deviceOptions);
- }
+ deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
+ if (deviceOptions == null)
+ {
+ deviceOptions = new DeviceOptions(deviceId);
+ dbContext.DeviceOptions.Add(deviceOptions);
+ }
- deviceOptions.CustomName = deviceName;
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ deviceOptions.CustomName = deviceName;
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, deviceOptions)));
}
@@ -66,22 +71,29 @@ namespace Jellyfin.Server.Implementations.Devices
///
public async Task CreateDevice(Device device)
{
- await using var dbContext = _dbProvider.CreateContext();
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.Devices.Add(device);
- dbContext.Devices.Add(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
return device;
}
///
public async Task GetDeviceOptions(string deviceId)
{
- await using var dbContext = _dbProvider.CreateContext();
- var deviceOptions = await dbContext.DeviceOptions
- .AsQueryable()
- .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
- .ConfigureAwait(false);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ DeviceOptions? deviceOptions;
+ await using (dbContext.ConfigureAwait(false))
+ {
+ deviceOptions = await dbContext.DeviceOptions
+ .AsNoTracking()
+ .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
+ .ConfigureAwait(false);
+ }
return deviceOptions ?? new DeviceOptions(deviceId);
}
@@ -97,14 +109,17 @@ namespace Jellyfin.Server.Implementations.Devices
///
public async Task GetDevice(string id)
{
- await using var dbContext = _dbProvider.CreateContext();
- var device = await dbContext.Devices
- .AsQueryable()
- .Where(d => d.DeviceId == id)
- .OrderByDescending(d => d.DateLastActivity)
- .Include(d => d.User)
- .FirstOrDefaultAsync()
- .ConfigureAwait(false);
+ Device? device;
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ device = await dbContext.Devices
+ .Where(d => d.DeviceId == id)
+ .OrderByDescending(d => d.DateLastActivity)
+ .Include(d => d.User)
+ .FirstOrDefaultAsync()
+ .ConfigureAwait(false);
+ }
var deviceInfo = device == null ? null : ToDeviceInfo(device);
@@ -114,41 +129,40 @@ namespace Jellyfin.Server.Implementations.Devices
///
public async Task> GetDevices(DeviceQuery query)
{
- await using var dbContext = _dbProvider.CreateContext();
-
- var devices = dbContext.Devices.AsQueryable();
-
- if (query.UserId.HasValue)
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
+ var devices = dbContext.Devices.AsQueryable();
+
+ if (query.UserId.HasValue)
+ {
+ devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
+ }
+
+ if (query.DeviceId != null)
+ {
+ devices = devices.Where(device => device.DeviceId == query.DeviceId);
+ }
+
+ if (query.AccessToken != null)
+ {
+ devices = devices.Where(device => device.AccessToken == query.AccessToken);
+ }
+
+ var count = await devices.CountAsync().ConfigureAwait(false);
+
+ if (query.Skip.HasValue)
+ {
+ devices = devices.Skip(query.Skip.Value);
+ }
+
+ if (query.Limit.HasValue)
+ {
+ devices = devices.Take(query.Limit.Value);
+ }
+
+ return new QueryResult(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
}
-
- if (query.DeviceId != null)
- {
- devices = devices.Where(device => device.DeviceId == query.DeviceId);
- }
-
- if (query.AccessToken != null)
- {
- devices = devices.Where(device => device.AccessToken == query.AccessToken);
- }
-
- var count = await devices.CountAsync().ConfigureAwait(false);
-
- if (query.Skip.HasValue)
- {
- devices = devices.Skip(query.Skip.Value);
- }
-
- if (query.Limit.HasValue)
- {
- devices = devices.Take(query.Limit.Value);
- }
-
- return new QueryResult(
- query.Skip,
- count,
- await devices.ToListAsync().ConfigureAwait(false));
}
///
@@ -165,37 +179,43 @@ namespace Jellyfin.Server.Implementations.Devices
///
public async Task> GetDevicesForUser(Guid? userId, bool? supportsSync)
{
- await using var dbContext = _dbProvider.CreateContext();
- var sessions = dbContext.Devices
- .Include(d => d.User)
- .AsQueryable()
- .OrderByDescending(d => d.DateLastActivity)
- .ThenBy(d => d.DeviceId)
- .AsAsyncEnumerable();
-
- if (supportsSync.HasValue)
+ IAsyncEnumerable sessions;
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
+ sessions = dbContext.Devices
+ .Include(d => d.User)
+ .OrderByDescending(d => d.DateLastActivity)
+ .ThenBy(d => d.DeviceId)
+ .AsAsyncEnumerable();
+
+ if (supportsSync.HasValue)
+ {
+ sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
+ }
+
+ if (userId.HasValue)
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
+ }
+
+ var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
+
+ return new QueryResult(array);
}
-
- if (userId.HasValue)
- {
- var user = _userManager.GetUserById(userId.Value);
-
- sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
- }
-
- var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
-
- return new QueryResult(array);
}
///
public async Task DeleteDevice(Device device)
{
- await using var dbContext = _dbProvider.CreateContext();
- dbContext.Devices.Remove(device);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.Devices.Remove(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
}
///
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..f98a0aede8
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,43 @@
+using System;
+using System.IO;
+using EFCoreSecondLevelCacheInterceptor;
+using MediaBrowser.Common.Configuration;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Extensions;
+
+///
+/// Extensions for the interface.
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Adds the interface to the service collection with second level caching enabled.
+ ///
+ /// An instance of the interface.
+ /// The updated service collection.
+ public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
+ {
+ serviceCollection.AddEFSecondLevelCache(options =>
+ options.UseMemoryCacheProvider()
+ .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
+ .DisableLogging(true)
+ .UseCacheKeyPrefix("EF_")
+ // Don't cache null values. Remove this optional setting if it's not necessary.
+ .SkipCachingResults(result =>
+ result.Value == null || (result.Value is EFTableRows rows && rows.RowsCount == 0)));
+
+ serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) =>
+ {
+ var applicationPaths = serviceProvider.GetRequiredService();
+ var loggerFactory = serviceProvider.GetRequiredService();
+ opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
+ .AddInterceptors(serviceProvider.GetRequiredService())
+ .UseLoggerFactory(loggerFactory);
+ });
+
+ return serviceCollection;
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 83b2262782..8c7eee5052 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -26,14 +26,15 @@
+
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
deleted file mode 100644
index c2c5198d14..0000000000
--- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Common.Configuration;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Implementations
-{
- ///
- /// Factory class for generating new instances.
- ///
- public class JellyfinDbProvider
- {
- private readonly IServiceProvider _serviceProvider;
- private readonly IApplicationPaths _appPaths;
- private readonly ILogger _logger;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The application's service provider.
- /// The application paths.
- /// The logger.
- public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger logger)
- {
- _serviceProvider = serviceProvider;
- _appPaths = appPaths;
- _logger = logger;
-
- using var jellyfinDb = CreateContext();
- if (jellyfinDb.Database.GetPendingMigrations().Any())
- {
- _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
- jellyfinDb.Database.Migrate();
- _logger.LogInformation("EFCore migrations applied successfully");
- }
- }
-
- ///
- /// Creates a new context.
- ///
- /// The newly created context.
- public JellyfinDb CreateContext()
- {
- var contextOptions = new DbContextOptionsBuilder().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
- return ActivatorUtilities.CreateInstance(_serviceProvider, contextOptions.Options);
- }
- }
-}
diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
new file mode 100644
index 0000000000..03e3f3c921
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
@@ -0,0 +1,657 @@
+#pragma warning disable CS1591
+
+//
+using System;
+using Jellyfin.Server.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(JellyfinDb))]
+ [Migration("20221022080052_AddIndexActivityLogsDateCreated")]
+ partial class AddIndexActivityLogsDateCreated
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "6.0.9");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EasyPassword")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users", "jellyfin");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.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/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs
new file mode 100644
index 0000000000..f09ad2709a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591, SA1601
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddIndexActivityLogsDateCreated : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_ActivityLogs_DateCreated",
+ schema: "jellyfin",
+ table: "ActivityLogs",
+ column: "DateCreated");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_ActivityLogs_DateCreated",
+ schema: "jellyfin",
+ table: "ActivityLogs");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index fcc360e260..2dd7b094aa 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+#nullable disable
+
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDb))]
@@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "5.0.7");
+ .HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId");
- b.ToTable("AccessSchedules");
+ b.ToTable("AccessSchedules", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
@@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
- b.ToTable("ActivityLogs");
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
@@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
- b.ToTable("CustomItemDisplayPreferences");
+ b.ToTable("CustomItemDisplayPreferences", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
@@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
- b.ToTable("DisplayPreferences");
+ b.ToTable("DisplayPreferences", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
@@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("DisplayPreferencesId");
- b.ToTable("HomeSection");
+ b.ToTable("HomeSection", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
@@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId")
.IsUnique();
- b.ToTable("ImageInfos");
+ b.ToTable("ImageInfos", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
@@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId");
- b.ToTable("ItemDisplayPreferences");
+ b.ToTable("ItemDisplayPreferences", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
@@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
- b.ToTable("Permissions");
+ b.ToTable("Permissions", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
@@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
- b.ToTable("Preferences");
+ b.ToTable("Preferences", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
@@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("AccessToken")
.IsUnique();
- b.ToTable("ApiKeys");
+ b.ToTable("ApiKeys", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
@@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId", "DeviceId");
- b.ToTable("Devices");
+ b.ToTable("Devices", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
@@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("DeviceId")
.IsUnique();
- b.ToTable("DeviceOptions");
+ b.ToTable("DeviceOptions", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
@@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("Username")
.IsUnique();
- b.ToTable("Users");
+ b.ToTable("Users", "jellyfin");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs
new file mode 100644
index 0000000000..9a63ed9f21
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs
@@ -0,0 +1,17 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+///
+/// FluentAPI configuration for the ActivityLog entity.
+///
+public class ActivityLogConfiguration : IEntityTypeConfiguration
+{
+ ///
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasIndex(entity => entity.DateCreated);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
index b79e46469c..33c08c8c29 100644
--- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
@@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security
///
public class AuthenticationManager : IAuthenticationManager
{
- private readonly JellyfinDbProvider _dbProvider;
+ private readonly IDbContextFactory _dbProvider;
///
/// Initializes a new instance of the class.
///
/// The database provider.
- public AuthenticationManager(JellyfinDbProvider dbProvider)
+ public AuthenticationManager(IDbContextFactory dbProvider)
{
_dbProvider = dbProvider;
}
@@ -24,50 +24,56 @@ namespace Jellyfin.Server.Implementations.Security
///
public async Task CreateApiKey(string name)
{
- await using var dbContext = _dbProvider.CreateContext();
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.ApiKeys.Add(new ApiKey(name));
- dbContext.ApiKeys.Add(new ApiKey(name));
-
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
}
///
public async Task> GetApiKeys()
{
- await using var dbContext = _dbProvider.CreateContext();
-
- return await dbContext.ApiKeys
- .AsAsyncEnumerable()
- .Select(key => new AuthenticationInfo
- {
- AppName = key.Name,
- AccessToken = key.AccessToken,
- DateCreated = key.DateCreated,
- DeviceId = string.Empty,
- DeviceName = string.Empty,
- AppVersion = string.Empty
- }).ToListAsync().ConfigureAwait(false);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ return await dbContext.ApiKeys
+ .AsAsyncEnumerable()
+ .Select(key => new AuthenticationInfo
+ {
+ AppName = key.Name,
+ AccessToken = key.AccessToken,
+ DateCreated = key.DateCreated,
+ DeviceId = string.Empty,
+ DeviceName = string.Empty,
+ AppVersion = string.Empty
+ }).ToListAsync().ConfigureAwait(false);
+ }
}
///
public async Task DeleteApiKey(string accessToken)
{
- await using var dbContext = _dbProvider.CreateContext();
-
- var key = await dbContext.ApiKeys
- .AsQueryable()
- .Where(apiKey => apiKey.AccessToken == accessToken)
- .FirstOrDefaultAsync()
- .ConfigureAwait(false);
-
- if (key == null)
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- return;
+ var key = await dbContext.ApiKeys
+ .AsQueryable()
+ .Where(apiKey => apiKey.AccessToken == accessToken)
+ .FirstOrDefaultAsync()
+ .ConfigureAwait(false);
+
+ if (key == null)
+ {
+ return;
+ }
+
+ dbContext.Remove(key);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
-
- dbContext.Remove(key);
-
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 9f813f532c..4d1a1b3cf8 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
+using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -15,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security
{
public class AuthorizationContext : IAuthorizationContext
{
- private readonly JellyfinDbProvider _jellyfinDbProvider;
+ private readonly IDbContextFactory _jellyfinDbProvider;
private readonly IUserManager _userManager;
private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext(
- JellyfinDbProvider jellyfinDb,
+ IDbContextFactory jellyfinDb,
IUserManager userManager,
IServerApplicationHost serverApplicationHost)
{
@@ -121,96 +122,99 @@ namespace Jellyfin.Server.Implementations.Security
#pragma warning restore CA1508
authInfo.HasToken = true;
- await using var dbContext = _jellyfinDbProvider.CreateContext();
- var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
-
- if (device != null)
+ var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- authInfo.IsAuthenticated = true;
- var updateToken = false;
+ var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
- // TODO: Remove these checks for IsNullOrWhiteSpace
- if (string.IsNullOrWhiteSpace(authInfo.Client))
- {
- authInfo.Client = device.AppName;
- }
-
- if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
- {
- authInfo.DeviceId = device.DeviceId;
- }
-
- // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
-
- if (string.IsNullOrWhiteSpace(authInfo.Device))
- {
- authInfo.Device = device.DeviceName;
- }
- else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
- {
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- device.DeviceName = authInfo.Device;
- }
- }
-
- if (string.IsNullOrWhiteSpace(authInfo.Version))
- {
- authInfo.Version = device.AppVersion;
- }
- else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
- {
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- device.AppVersion = authInfo.Version;
- }
- }
-
- if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
- {
- device.DateLastActivity = DateTime.UtcNow;
- updateToken = true;
- }
-
- authInfo.User = _userManager.GetUserById(device.UserId);
-
- if (updateToken)
- {
- dbContext.Devices.Update(device);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
- }
- }
- else
- {
- var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
- if (key != null)
+ if (device != null)
{
authInfo.IsAuthenticated = true;
- authInfo.Client = key.Name;
- authInfo.Token = key.AccessToken;
+ var updateToken = false;
+
+ // TODO: Remove these checks for IsNullOrWhiteSpace
+ if (string.IsNullOrWhiteSpace(authInfo.Client))
+ {
+ authInfo.Client = device.AppName;
+ }
+
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
- authInfo.DeviceId = _serverApplicationHost.SystemId;
+ authInfo.DeviceId = device.DeviceId;
}
+ // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
+ var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
+
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
- authInfo.Device = _serverApplicationHost.Name;
+ authInfo.Device = device.DeviceName;
+ }
+ else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
+ {
+ updateToken = true;
+ device.DeviceName = authInfo.Device;
+ }
}
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
- authInfo.Version = _serverApplicationHost.ApplicationVersionString;
+ authInfo.Version = device.AppVersion;
+ }
+ else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
+ {
+ if (allowTokenInfoUpdate)
+ {
+ updateToken = true;
+ device.AppVersion = authInfo.Version;
+ }
}
- authInfo.IsApiKey = true;
- }
- }
+ if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
+ {
+ device.DateLastActivity = DateTime.UtcNow;
+ updateToken = true;
+ }
- return authInfo;
+ authInfo.User = _userManager.GetUserById(device.UserId);
+
+ if (updateToken)
+ {
+ dbContext.Devices.Update(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
+ if (key != null)
+ {
+ authInfo.IsAuthenticated = true;
+ authInfo.Client = key.Name;
+ authInfo.Token = key.AccessToken;
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+ {
+ authInfo.DeviceId = _serverApplicationHost.SystemId;
+ }
+
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
+ {
+ authInfo.Device = _serverApplicationHost.Name;
+ }
+
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
+ {
+ authInfo.Version = _serverApplicationHost.ApplicationVersionString;
+ }
+
+ authInfo.IsApiKey = true;
+ }
+ }
+
+ return authInfo;
+ }
}
///
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index 65edb30ad2..87babc05c8 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -20,10 +20,10 @@ namespace Jellyfin.Server.Implementations.Users
///
/// Initializes a new instance of the class.
///
- /// The database context.
- public DisplayPreferencesManager(JellyfinDb dbContext)
+ /// The database context factory.
+ public DisplayPreferencesManager(IDbContextFactory dbContextFactory)
{
- _dbContext = dbContext;
+ _dbContext = dbContextFactory.CreateDbContext();
}
///
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index ed90e81c6f..25560707ac 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users
///
public class UserManager : IUserManager
{
- private readonly JellyfinDbProvider _dbProvider;
+ private readonly IDbContextFactory _dbProvider;
private readonly IEventManager _eventManager;
private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager;
@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
/// The image processor.
/// The logger.
public UserManager(
- JellyfinDbProvider dbProvider,
+ IDbContextFactory dbProvider,
IEventManager eventManager,
ICryptoProvider cryptoProvider,
INetworkManager networkManager,
@@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Users
_defaultPasswordResetProvider = _passwordResetProviders.OfType().First();
_users = new ConcurrentDictionary();
- using var dbContext = _dbProvider.CreateContext();
+ using var dbContext = _dbProvider.CreateDbContext();
foreach (var user in dbContext.Users
.Include(user => user.Permissions)
.Include(user => user.Preferences)
@@ -139,31 +139,35 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("The new and old names must be different.");
}
- await using var dbContext = _dbProvider.CreateContext();
-
- if (await dbContext.Users
- .AsQueryable()
- .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
- .ConfigureAwait(false))
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- throw new ArgumentException(string.Format(
- CultureInfo.InvariantCulture,
- "A user with the name '{0}' already exists.",
- newName));
+ if (await dbContext.Users
+ .AsQueryable()
+ .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
+ .ConfigureAwait(false))
+ {
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "A user with the name '{0}' already exists.",
+ newName));
+ }
+
+ user.Username = newName;
+ await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
}
- user.Username = newName;
- await UpdateUserAsync(user).ConfigureAwait(false);
OnUserUpdated?.Invoke(this, new GenericEventArgs(user));
}
///
public async Task UpdateUserAsync(User user)
{
- await using var dbContext = _dbProvider.CreateContext();
- dbContext.Users.Update(user);
- _users[user.Id] = user;
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
+ }
}
internal async Task CreateUserInternalAsync(string name, JellyfinDb dbContext)
@@ -202,12 +206,15 @@ namespace Jellyfin.Server.Implementations.Users
name));
}
- await using var dbContext = _dbProvider.CreateContext();
+ User newUser;
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
- var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
-
- dbContext.Users.Add(newUser);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ dbContext.Users.Add(newUser);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
@@ -241,9 +248,13 @@ namespace Jellyfin.Server.Implementations.Users
nameof(userId));
}
- await using var dbContext = _dbProvider.CreateContext();
- dbContext.Users.Remove(user);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.Users.Remove(user);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
_users.Remove(userId);
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
@@ -288,7 +299,7 @@ namespace Jellyfin.Server.Implementations.Users
user.EasyPassword = newPasswordSha1;
await UpdateUserAsync(user).ConfigureAwait(false);
- _eventManager.Publish(new UserPasswordChangedEventArgs(user));
+ await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
}
///
@@ -541,14 +552,17 @@ namespace Jellyfin.Server.Implementations.Users
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
- await using var dbContext = _dbProvider.CreateContext();
- var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
- newUser.SetPermission(PermissionKind.IsAdministrator, true);
- newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
- newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
+ newUser.SetPermission(PermissionKind.IsAdministrator, true);
+ newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
+ newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
- dbContext.Users.Add(newUser);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ dbContext.Users.Add(newUser);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
}
///
@@ -584,105 +598,111 @@ namespace Jellyfin.Server.Implementations.Users
///
public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
{
- await using var dbContext = _dbProvider.CreateContext();
- var user = dbContext.Users
- .Include(u => u.Permissions)
- .Include(u => u.Preferences)
- .Include(u => u.AccessSchedules)
- .Include(u => u.ProfileImage)
- .FirstOrDefault(u => u.Id.Equals(userId))
- ?? throw new ArgumentException("No user exists with given Id!");
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id.Equals(userId))
+ ?? throw new ArgumentException("No user exists with given Id!");
- user.SubtitleMode = config.SubtitleMode;
- user.HidePlayedInLatest = config.HidePlayedInLatest;
- user.EnableLocalPassword = config.EnableLocalPassword;
- user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
- user.DisplayCollectionsView = config.DisplayCollectionsView;
- user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
- user.AudioLanguagePreference = config.AudioLanguagePreference;
- user.RememberAudioSelections = config.RememberAudioSelections;
- user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
- user.RememberSubtitleSelections = config.RememberSubtitleSelections;
- user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+ user.SubtitleMode = config.SubtitleMode;
+ user.HidePlayedInLatest = config.HidePlayedInLatest;
+ user.EnableLocalPassword = config.EnableLocalPassword;
+ user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
+ user.DisplayCollectionsView = config.DisplayCollectionsView;
+ user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
+ user.AudioLanguagePreference = config.AudioLanguagePreference;
+ user.RememberAudioSelections = config.RememberAudioSelections;
+ user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
+ user.RememberSubtitleSelections = config.RememberSubtitleSelections;
+ user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
- user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
- user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
- user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
- user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+ user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+ user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+ user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+ user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
- dbContext.Update(user);
- _users[user.Id] = user;
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ dbContext.Update(user);
+ _users[user.Id] = user;
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
}
///
public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
{
- await using var dbContext = _dbProvider.CreateContext();
- var user = dbContext.Users
- .Include(u => u.Permissions)
- .Include(u => u.Preferences)
- .Include(u => u.AccessSchedules)
- .Include(u => u.ProfileImage)
- .FirstOrDefault(u => u.Id.Equals(userId))
- ?? throw new ArgumentException("No user exists with given Id!");
-
- // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
- int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- -1 => null,
- 0 => 3,
- _ => policy.LoginAttemptsBeforeLockout
- };
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id.Equals(userId))
+ ?? throw new ArgumentException("No user exists with given Id!");
- user.MaxParentalAgeRating = policy.MaxParentalRating;
- user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
- user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
- user.AuthenticationProviderId = policy.AuthenticationProviderId;
- user.PasswordResetProviderId = policy.PasswordResetProviderId;
- user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
- user.LoginAttemptsBeforeLockout = maxLoginAttempts;
- user.MaxActiveSessions = policy.MaxActiveSessions;
- user.SyncPlayAccess = policy.SyncPlayAccess;
- user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
- user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
- user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
- user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
- user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
- user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
- user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
- user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
- user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
- user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
- user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
- user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
- user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
- user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
- user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
- user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
- user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
- user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
- user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+ // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
+ int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
+ {
+ -1 => null,
+ 0 => 3,
+ _ => policy.LoginAttemptsBeforeLockout
+ };
- user.AccessSchedules.Clear();
- foreach (var policyAccessSchedule in policy.AccessSchedules)
- {
- user.AccessSchedules.Add(policyAccessSchedule);
+ user.MaxParentalAgeRating = policy.MaxParentalRating;
+ user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
+ user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
+ user.AuthenticationProviderId = policy.AuthenticationProviderId;
+ user.PasswordResetProviderId = policy.PasswordResetProviderId;
+ user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
+ user.LoginAttemptsBeforeLockout = maxLoginAttempts;
+ user.MaxActiveSessions = policy.MaxActiveSessions;
+ user.SyncPlayAccess = policy.SyncPlayAccess;
+ user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+ user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+ user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+ user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+ user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+ user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+ user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+ user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+ user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+ user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+ user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+ user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+ user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+ user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+ user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+ user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+ user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+ user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+ user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+ user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+
+ user.AccessSchedules.Clear();
+ foreach (var policyAccessSchedule in policy.AccessSchedules)
+ {
+ user.AccessSchedules.Add(policyAccessSchedule);
+ }
+
+ // TODO: fix this at some point
+ user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty());
+ user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+ user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+ user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+
+ dbContext.Update(user);
+ _users[user.Id] = user;
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
-
- // TODO: fix this at some point
- user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty());
- user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
- user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
- user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
-
- dbContext.Update(user);
- _users[user.Id] = user;
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
///
@@ -693,9 +713,13 @@ namespace Jellyfin.Server.Implementations.Users
return;
}
- await using var dbContext = _dbProvider.CreateContext();
- dbContext.Remove(user.ProfileImage);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ dbContext.Remove(user.ProfileImage);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
user.ProfileImage = null;
_users[user.Id] = user;
}
@@ -859,5 +883,12 @@ namespace Jellyfin.Server.Implementations.Users
await UpdateUserAsync(user).ConfigureAwait(false);
}
+
+ private async Task UpdateUserInternalAsync(JellyfinDb dbContext, User user)
+ {
+ dbContext.Users.Update(user);
+ _users[user.Id] = user;
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
}
}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 984711dc2d..002193baf5 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Reflection;
using Emby.Drawing;
using Emby.Server.Implementations;
@@ -71,19 +70,13 @@ namespace Jellyfin.Server
Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder));
}
- serviceCollection.AddDbContextPool(
- options => options
- .UseLoggerFactory(LoggerFactory)
- .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
-
serviceCollection.AddEventServices();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
+ serviceCollection.AddScoped();
serviceCollection.AddSingleton();
// TODO search the assemblies instead of adding them manually?
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 66fa3bc31b..f74152405a 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -434,11 +434,15 @@ namespace Jellyfin.Server.Extensions
options.MapType(() =>
new OpenApiSchema
{
- Type = "string",
- Enum = Enum.GetNames()
- .Select(e => new OpenApiString(e))
- .Cast()
- .ToArray()
+ Type = "array",
+ Items = new OpenApiSchema
+ {
+ Reference = new OpenApiReference
+ {
+ Id = nameof(TranscodeReason),
+ Type = ReferenceType.Schema,
+ }
+ }
});
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
index 487948f815..645696e319 100644
--- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs
+++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using Jellyfin.Extensions;
using Jellyfin.Server.Migrations;
using MediaBrowser.Common.Plugins;
@@ -8,6 +9,7 @@ using MediaBrowser.Model.ApiClient;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
+using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -56,6 +58,15 @@ namespace Jellyfin.Server.Filters
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
}
+
+ context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema
+ {
+ Type = "string",
+ Enum = Enum.GetNames()
+ .Select(e => new OpenApiString(e))
+ .Cast()
+ .ToArray()
+ });
}
}
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index b2d79050b1..44f92cf833 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -37,10 +37,10 @@
-
-
-
-
+
+
+
+
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index 9875df3108..6ee5bf38a4 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -65,8 +65,9 @@ namespace Jellyfin.Server.Middleware
// Always redirect back to the default path if the base prefix is invalid or missing
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
- var uri = new Uri(localPath);
- var redirectUri = new Uri(baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]);
+ var port = httpContext.Request.Host.Port ?? -1;
+ var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri;
+ var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri;
var target = uri.MakeRelativeUri(redirectUri).ToString();
_logger.LogDebug("Redirecting to {Target}", target);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index 9e22978aee..bf66f75ff9 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines
private const string DbFilename = "activitylog.db";
private readonly ILogger _logger;
- private readonly JellyfinDbProvider _provider;
+ private readonly IDbContextFactory _provider;
private readonly IServerApplicationPaths _paths;
///
@@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines
/// The logger.
/// The server application paths.
/// The database provider.
- public MigrateActivityLogDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
+ public MigrateActivityLogDb(ILogger logger, IServerApplicationPaths paths, IDbContextFactory provider)
{
_logger = logger;
_provider = provider;
@@ -68,7 +68,7 @@ namespace Jellyfin.Server.Migrations.Routines
{
using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null);
_logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
- using var dbContext = _provider.CreateContext();
+ using var dbContext = _provider.CreateDbContext();
var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index ba0e33585c..bf1ea8233d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -6,6 +6,7 @@ using Jellyfin.Data.Entities.Security;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -19,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines
private const string DbFilename = "authentication.db";
private readonly ILogger _logger;
- private readonly JellyfinDbProvider _dbProvider;
+ private readonly IDbContextFactory _dbProvider;
private readonly IServerApplicationPaths _appPaths;
private readonly IUserManager _userManager;
@@ -32,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines
/// The user manager.
public MigrateAuthenticationDb(
ILogger logger,
- JellyfinDbProvider dbProvider,
+ IDbContextFactory dbProvider,
IServerApplicationPaths appPaths,
IUserManager userManager)
{
@@ -60,7 +61,7 @@ namespace Jellyfin.Server.Migrations.Routines
ConnectionFlags.ReadOnly,
null))
{
- using var dbContext = _dbProvider.CreateContext();
+ using var dbContext = _dbProvider.CreateDbContext();
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 74f2349f5f..37716482c3 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -10,6 +10,7 @@ using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -24,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger _logger;
private readonly IServerApplicationPaths _paths;
- private readonly JellyfinDbProvider _provider;
+ private readonly IDbContextFactory _provider;
private readonly JsonSerializerOptions _jsonOptions;
private readonly IUserManager _userManager;
@@ -38,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines
public MigrateDisplayPreferencesDb(
ILogger logger,
IServerApplicationPaths paths,
- JellyfinDbProvider provider,
+ IDbContextFactory provider,
IUserManager userManager)
{
_logger = logger;
@@ -84,7 +85,7 @@ namespace Jellyfin.Server.Migrations.Routines
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
{
- using var dbContext = _provider.CreateContext();
+ using var dbContext = _provider.CreateDbContext();
var results = connection.Query("SELECT * FROM userdisplaypreferences");
foreach (var result in results)
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 9b2d603c7f..0c2cc69a7e 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -26,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger _logger;
private readonly IServerApplicationPaths _paths;
- private readonly JellyfinDbProvider _provider;
+ private readonly IDbContextFactory _provider;
private readonly IXmlSerializer _xmlSerializer;
///
@@ -39,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
public MigrateUserDb(
ILogger logger,
IServerApplicationPaths paths,
- JellyfinDbProvider provider,
+ IDbContextFactory provider,
IXmlSerializer xmlSerializer)
{
_logger = logger;
@@ -65,7 +66,7 @@ namespace Jellyfin.Server.Migrations.Routines
using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
{
- var dbContext = _provider.CreateContext();
+ var dbContext = _provider.CreateDbContext();
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index a6f0b705dc..7ed8388256 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -192,6 +193,7 @@ namespace Jellyfin.Server
// Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = webHost.Services;
+
await appHost.InitializeServices().ConfigureAwait(false);
Migrations.MigrationRunner.Run(appHost, _loggerFactory);
@@ -236,10 +238,13 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
- using var context = appHost.Resolve().CreateContext();
- if (context.Database.IsSqlite())
+ var context = await appHost.ServiceProvider.GetRequiredService>().CreateDbContextAsync().ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
{
- context.Database.ExecuteSqlRaw("PRAGMA optimize");
+ if (context.Database.IsSqlite())
+ {
+ await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false);
+ }
}
}
@@ -608,11 +613,14 @@ namespace Jellyfin.Server
catch (Exception ex)
{
Log.Logger = new LoggerConfiguration()
- .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
+ .WriteTo.Console(
+ outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
+ formatProvider: CultureInfo.InvariantCulture)
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
rollingInterval: RollingInterval.Day,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
+ formatProvider: CultureInfo.InvariantCulture,
encoding: Encoding.UTF8))
.Enrich.FromLogContext()
.Enrich.WithThreadId()
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 1954a5c558..49a57aa688 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -9,6 +9,7 @@ using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Infrastructure;
using Jellyfin.Server.Middleware;
using MediaBrowser.Common.Net;
@@ -65,7 +66,7 @@ namespace Jellyfin.Server
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
-
+ services.AddJellyfinDbContext();
services.AddJellyfinApiSwagger();
// configure custom legacy authentication
diff --git a/MediaBrowser.Common/Providers/ProviderIdParsers.cs b/MediaBrowser.Common/Providers/ProviderIdParsers.cs
index 487b5a6d29..d569167b1d 100644
--- a/MediaBrowser.Common/Providers/ProviderIdParsers.cs
+++ b/MediaBrowser.Common/Providers/ProviderIdParsers.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Common.Providers
/// True if parsing was successful, false otherwise.
public static bool TryFindImdbId(ReadOnlySpan text, out ReadOnlySpan imdbId)
{
- // imdb id is at least 9 chars (tt + 7 numbers)
+ // IMDb id is at least 9 chars (tt + 7 numbers)
while (text.Length >= 2 + ImdbMinNumbers)
{
var ttPos = text.IndexOf(ImdbPrefix);
@@ -42,7 +42,7 @@ namespace MediaBrowser.Common.Providers
}
}
- // skip if more than 8 digits + 2 chars for tt
+ // Skip if more than 8 digits + 2 chars for tt
if (i <= ImdbMaxNumbers + 2 && i >= ImdbMinNumbers + 2)
{
imdbId = text.Slice(0, i);
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index d273b54fc6..61539cae56 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -35,7 +35,7 @@ namespace MediaBrowser.Controller.BaseItemManager
public SemaphoreSlim MetadataRefreshThrottler { get; private set; }
///
- public bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name)
+ public bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name)
{
if (baseItem is Channel)
{
@@ -49,10 +49,9 @@ namespace MediaBrowser.Controller.BaseItemManager
return !baseItem.EnableMediaSourceDisplay;
}
- var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
- if (typeOptions != null)
+ if (libraryTypeOptions != null)
{
- return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ return libraryTypeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
@@ -61,7 +60,7 @@ namespace MediaBrowser.Controller.BaseItemManager
}
///
- public bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name)
+ public bool IsImageFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name)
{
if (baseItem is Channel)
{
@@ -75,10 +74,9 @@ namespace MediaBrowser.Controller.BaseItemManager
return !baseItem.EnableMediaSourceDisplay;
}
- var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
- if (typeOptions != null)
+ if (libraryTypeOptions != null)
{
- return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ return libraryTypeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
index e18994214e..b07c80879d 100644
--- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -18,18 +18,18 @@ namespace MediaBrowser.Controller.BaseItemManager
/// Is metadata fetcher enabled.
///
/// The base item.
- /// The library options.
+ /// The type options for baseItem from the library (if defined).
/// The metadata fetcher name.
/// true if metadata fetcher is enabled, else false.
- bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
+ bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name);
///
/// Is image fetcher enabled.
///
/// The base item.
- /// The library options.
+ /// The type options for baseItem from the library (if defined).
/// The image fetcher name.
/// true if image fetcher is enabled, else false.
- bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
+ bool IsImageFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name);
}
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 24163f1df9..7f5f9f74bd 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -75,7 +75,9 @@ namespace MediaBrowser.Controller.Entities
Model.Entities.ExtraType.DeletedScene,
Model.Entities.ExtraType.Interview,
Model.Entities.ExtraType.Sample,
- Model.Entities.ExtraType.Scene
+ Model.Entities.ExtraType.Scene,
+ Model.Entities.ExtraType.Featurette,
+ Model.Entities.ExtraType.Short
};
private string _sortName;
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 13bfd07c34..1bf5285382 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -205,6 +205,16 @@ namespace MediaBrowser.Controller.Entities
public int? MinIndexNumber { get; set; }
+ ///
+ /// Gets or sets the minimum ParentIndexNumber and IndexNumber.
+ ///
+ ///
+ /// It produces this where clause:
+ /// (ParentIndexNumber = X and IndexNumber >= Y) or ParentIndexNumber > X.
+ ///
+ ///
+ public (int ParentIndexNumber, int IndexNumber)? MinParentAndIndexNumber { get; set; }
+
public int? AiredDuringSeason { get; set; }
public double? MinCriticRating { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 77e70f8fbd..3c12acd90d 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -33,9 +33,9 @@ namespace MediaBrowser.Controller.Entities.Movies
.ToArray();
///
- /// Gets or sets the name of the TMDB collection.
+ /// Gets or sets the name of the TMDb collection.
///
- /// The name of the TMDB collection.
+ /// The name of the TMDb collection.
public string TmdbCollectionName { get; set; }
[JsonIgnore]
diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs
index 455054bd12..de74aa5a11 100644
--- a/MediaBrowser.Controller/Library/ILibraryMonitor.cs
+++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs
@@ -34,12 +34,5 @@ namespace MediaBrowser.Controller.Library
///
/// The path.
void ReportFileSystemChanged(string path);
-
- ///
- /// Determines whether [is path locked] [the specified path].
- ///
- /// The path.
- /// true if [is path locked] [the specified path]; otherwise, false.
- bool IsPathLocked(string path);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index d0362b128c..74abb91b28 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -31,10 +31,13 @@ namespace MediaBrowser.Controller.MediaEncoding
private const string VideotoolboxAlias = "vt";
private const string OpenclAlias = "ocl";
private const string CudaAlias = "cu";
+ private const string DrmAlias = "dr";
+ private const string VulkanAlias = "vk";
private readonly IApplicationPaths _appPaths;
private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
+ private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
private readonly Version _minKernelVersioni915Hang = new Version(5, 18);
private static readonly string[] _videoProfilesH264 = new[]
@@ -149,6 +152,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilter("hwupload_cuda");
}
+ private bool IsVulkanFullSupported()
+ {
+ return _mediaEncoder.SupportsHwaccel("vulkan")
+ && _mediaEncoder.SupportsFilter("libplacebo")
+ && _mediaEncoder.SupportsFilter("scale_vulkan")
+ && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync);
+ }
+
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream == null
@@ -176,6 +187,19 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase));
}
+ private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
+ {
+ if (state.VideoStream == null)
+ {
+ return false;
+ }
+
+ // libplacebo has partial Dolby Vision to SDR tonemapping support.
+ return options.EnableTonemapping
+ && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && GetVideoColorBitDepth(state) == 10;
+ }
+
private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream == null
@@ -756,8 +780,13 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (_mediaEncoder.IsVaapiDeviceAmd)
{
- args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
- filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ if (!IsVulkanFullSupported()
+ || !_mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
+ || Environment.OSVersion.Version < _minKernelVersionAmdVkFmtModifier)
+ {
+ args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ }
}
else
{
@@ -1147,24 +1176,6 @@ namespace MediaBrowser.Controller.MediaEncoding
":fontsdir='{0}'",
_mediaEncoder.EscapeSubtitleFilterPath(fontPath));
- // TODO
- // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf");
- // string fallbackFontParam = string.Empty;
-
- // if (!File.Exists(fallbackFontPath))
- // {
- // _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath));
- // using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf"))
- // {
- // using (var fileStream = new FileStream(fallbackFontPath, FileMode.Create, FileAccess.Write, FileShare.Read))
- // {
- // stream.CopyTo(fileStream);
- // }
- // }
- // }
-
- // fallbackFontParam = string.Format(CultureInfo.InvariantCulture, ":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath)));
-
if (state.SubtitleStream.IsExternal)
{
var charsetParam = string.Empty;
@@ -1192,7 +1203,6 @@ namespace MediaBrowser.Controller.MediaEncoding
alphaParam,
sub2videoParam,
fontParam,
- // fallbackFontParam,
setPtsParam);
}
@@ -1430,7 +1440,11 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -preset 7";
}
- param += " -look_ahead 0";
+ // Only h264_qsv has look_ahead option
+ if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -look_ahead 0";
+ }
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
@@ -1468,7 +1482,7 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
default:
- param += " -preset p4";
+ param += " -preset p1";
break;
}
}
@@ -2774,22 +2788,41 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- var args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709";
+ var args = string.Empty;
+ var algorithm = options.TonemappingAlgorithm;
- if (hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
{
- args += ",procamp_vaapi=b={2}:c={3}:extra_hw_frames=16";
+ args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16";
return string.Format(
CultureInfo.InvariantCulture,
args,
- hwTonemapSuffix,
videoFormat ?? "nv12",
options.VppTonemappingBrightness,
options.VppTonemappingContrast);
}
+ else if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase))
+ {
+ args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none";
+
+ if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+ {
+ args += ":range={6}";
+ }
+
+ if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase))
+ {
+ algorithm = "bt.2390";
+ }
+
+ else if (string.Equals(options.TonemappingAlgorithm, "none", StringComparison.OrdinalIgnoreCase))
+ {
+ algorithm = "clip";
+ }
+ }
else
{
- args += ":tonemap={2}:peak={3}:desat={4}";
+ args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
if (options.TonemappingParam != 0)
{
@@ -2807,7 +2840,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args,
hwTonemapSuffix,
videoFormat ?? "nv12",
- options.TonemappingAlgorithm,
+ algorithm,
options.TonemappingPeak,
options.TonemappingDesat,
options.TonemappingParam,
@@ -3419,6 +3452,12 @@ namespace MediaBrowser.Controller.MediaEncoding
// map from d3d11va to qsv.
mainFilters.Add("hwmap=derive_device=qsv");
}
+ else
+ {
+ // Insert a qsv scaler to sync the decoder surface,
+ // msdk will passthrough this internally.
+ mainFilters.Add("hwmap=derive_device=qsv,scale_qsv");
+ }
}
// hw deint
@@ -3770,7 +3809,9 @@ namespace MediaBrowser.Controller.MediaEncoding
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
- var isVaapiOclSupported = isLinux && IsVaapiSupported(state) && IsVaapiFullSupported() && IsOpenclFullSupported();
+ var isVaapiFullSupported = isLinux && IsVaapiSupported(state) && IsVaapiFullSupported();
+ var isVaapiOclSupported = isVaapiFullSupported && IsOpenclFullSupported();
+ var isVaapiVkSupported = isVaapiFullSupported && IsVulkanFullSupported();
// legacy vaapi pipeline(copy-back)
if ((isSwDecoder && isSwEncoder)
@@ -3798,14 +3839,24 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
// Intel iHD path, with extra vpp tonemap and overlay support.
- return GetVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
+ return GetIntelVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
- // Intel i965 and Amd radeonsi/r600 path, only featuring scale and deinterlace support.
+ // prefered vaapi + vulkan filters pipeline
+ if (_mediaEncoder.IsVaapiDeviceAmd
+ && isVaapiVkSupported
+ && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
+ && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
+ {
+ // AMD radeonsi path(Vega/gfx9+, kernel>=5.15), with extra vulkan tonemap and overlay support.
+ return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
+ }
+
+ // Intel i965 and Amd radeonsi/r600 path(Polaris/gfx8-), only featuring scale and deinterlace support.
return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
- public (List MainFilters, List SubFilters, List OverlayFilters) GetVaapiFullVidFiltersPrefered(
+ public (List MainFilters, List SubFilters, List OverlayFilters) GetIntelVaapiFullVidFiltersPrefered(
EncodingJobInfo state,
EncodingOptions options,
string vidDecoder,
@@ -4003,6 +4054,203 @@ namespace MediaBrowser.Controller.MediaEncoding
return (mainFilters, subFilters, overlayFilters);
}
+ public (List MainFilters, List SubFilters, List OverlayFilters) GetAmdVaapiFullVidFiltersPrefered(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidDecoder,
+ string vidEncoder)
+ {
+ var inW = state.VideoStream?.Width;
+ var inH = state.VideoStream?.Height;
+ var reqW = state.BaseRequest.Width;
+ var reqH = state.BaseRequest.Height;
+ var reqMaxW = state.BaseRequest.MaxWidth;
+ var reqMaxH = state.BaseRequest.MaxHeight;
+ var threeDFormat = state.MediaSource.Video3DFormat;
+
+ var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
+ var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
+ var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
+ var isSwEncoder = !isVaapiEncoder;
+ var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
+
+ var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+ var doVkTonemap = IsVulkanHwTonemapAvailable(state, options);
+ var doDeintH2645 = doDeintH264 || doDeintHevc;
+
+ var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
+ var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
+ var hasAssSubs = hasSubs
+ && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+
+ /* Make main filters for video stream */
+ var mainFilters = new List();
+
+ mainFilters.Add(GetOverwriteColorPropertiesParam(state, doVkTonemap));
+
+ if (isSwDecoder)
+ {
+ // INPUT sw surface(memory)
+ // sw deint
+ if (doDeintH2645)
+ {
+ var swDeintFilter = GetSwDeinterlaceFilter(state, options);
+ mainFilters.Add(swDeintFilter);
+ }
+
+ var outFormat = doVkTonemap ? "yuv420p10le" : "nv12";
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ // sw scale
+ mainFilters.Add(swScaleFilter);
+ mainFilters.Add("format=" + outFormat);
+
+ // keep video at memory except vk tonemap,
+ // since the overhead caused by hwupload >>> using sw filter.
+ // sw => hw
+ if (doVkTonemap)
+ {
+ mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
+ }
+ }
+ else if (isVaapiDecoder)
+ {
+ // INPUT vaapi surface(vram)
+ // hw deint
+ if (doDeintH2645)
+ {
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
+ mainFilters.Add(deintFilter);
+ }
+
+ var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12");
+ var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for overlay_vulkan
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs)
+ {
+ hwScaleFilter += ":extra_hw_frames=32";
+ }
+
+ // hw scale
+ mainFilters.Add(hwScaleFilter);
+ }
+
+ if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs)))
+ {
+ // map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ }
+
+ // vk tonemap
+ if (doVkTonemap)
+ {
+ var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12";
+ var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat);
+ mainFilters.Add(tonemapFilter);
+ }
+
+ if (doVkTonemap && isVaInVaOut && !hasSubs)
+ {
+ // OUTPUT vaapi(nv12/bgra) surface(vram)
+ // reverse-mapping via vaapi-vulkan interop.
+ mainFilters.Add("hwmap=derive_device=vaapi:reverse=1");
+ mainFilters.Add("format=vaapi");
+ }
+
+ var memoryOutput = false;
+ var isUploadForVkTonemap = isSwDecoder && doVkTonemap;
+ if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap)
+ {
+ memoryOutput = true;
+
+ // OUTPUT nv12 surface(memory)
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
+ }
+
+ // OUTPUT nv12 surface(memory)
+ if (isSwDecoder && isVaapiEncoder)
+ {
+ memoryOutput = true;
+ }
+
+ if (memoryOutput)
+ {
+ // text subtitles
+ if (hasTextSubs)
+ {
+ var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
+ mainFilters.Add(textSubtitlesFilter);
+ }
+ }
+
+ if (memoryOutput && isVaapiEncoder)
+ {
+ if (!hasGraphicalSubs)
+ {
+ mainFilters.Add("hwupload_vaapi");
+ }
+ }
+
+ /* Make sub and overlay filters for subtitle stream */
+ var subFilters = new List();
+ var overlayFilters = new List();
+ if (isVaInVaOut)
+ {
+ if (hasSubs)
+ {
+ if (hasGraphicalSubs)
+ {
+ // scale=s=1280x720,format=bgra,hwupload
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subSwScaleFilter);
+ subFilters.Add("format=bgra");
+ }
+ else if (hasTextSubs)
+ {
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
+ var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
+ subFilters.Add(alphaSrcFilter);
+ subFilters.Add("format=bgra");
+ subFilters.Add(subTextSubtitlesFilter);
+ }
+
+ subFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
+
+ overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
+
+ // explicitly sync using libplacebo.
+ overlayFilters.Add("libplacebo=format=nv12:upscaler=none:downscaler=none");
+
+ // OUTPUT vaapi(nv12/bgra) surface(vram)
+ // reverse-mapping via vaapi-vulkan interop.
+ overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1");
+ overlayFilters.Add("format=vaapi");
+ }
+ }
+ else if (memoryOutput)
+ {
+ if (hasGraphicalSubs)
+ {
+ var subSwScaleFilter = isSwDecoder
+ ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+ : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subSwScaleFilter);
+ overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
+
+ if (isVaapiEncoder)
+ {
+ overlayFilters.Add("hwupload_vaapi");
+ }
+ }
+ }
+
+ return (mainFilters, subFilters, overlayFilters);
+ }
+
public (List MainFilters, List SubFilters, List OverlayFilters) GetVaapiLimitedVidFiltersPrefered(
EncodingJobInfo state,
EncodingOptions options,
diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
index a4869cb670..b1d319d211 100644
--- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
+++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
@@ -28,6 +28,11 @@ namespace MediaBrowser.Controller.MediaEncoding
///
/// The overlay_vaapi_framesync.
///
- OverlayVaapiFrameSync = 4
+ OverlayVaapiFrameSync = 4,
+
+ ///
+ /// The overlay_vulkan_framesync.
+ ///
+ OverlayVulkanFrameSync = 5
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 69d0bf45cc..52c57b906e 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -61,6 +61,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// true if the Vaapi device is an Intel(legacy i965 driver) GPU, false otherwise.
bool IsVaapiDeviceInteli965 { get; }
+ ///
+ /// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier.
+ ///
+ /// true if the Vaapi device supports vulkan drm format modifier, false otherwise.
+ bool IsVaapiDeviceSupportVulkanFmtModifier { get; }
+
///
/// Whether given encoder codec is supported.
///
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 647de50030..2fe3a54723 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -227,9 +227,10 @@ namespace MediaBrowser.Controller.Net
connection.Item2.Cancel();
connection.Item2.Dispose();
}
- catch (ObjectDisposedException)
+ catch (ObjectDisposedException ex)
{
// TODO Investigate and properly fix.
+ Logger.LogError(ex, "Object Disposed");
}
lock (_activeConnections)
diff --git a/MediaBrowser.Controller/Providers/IHasOrder.cs b/MediaBrowser.Controller/Providers/IHasOrder.cs
index 9fde0e6958..77b0407a20 100644
--- a/MediaBrowser.Controller/Providers/IHasOrder.cs
+++ b/MediaBrowser.Controller/Providers/IHasOrder.cs
@@ -1,9 +1,14 @@
-#pragma warning disable CS1591
-
namespace MediaBrowser.Controller.Providers
{
+ ///
+ /// Interface IHasOrder.
+ ///
public interface IHasOrder
{
+ ///
+ /// Gets the order.
+ ///
+ /// The order.
int Order { get; }
}
}
diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs
index 05fbb18ee4..f0f1d18624 100644
--- a/MediaBrowser.Controller/Providers/IMetadataService.cs
+++ b/MediaBrowser.Controller/Providers/IMetadataService.cs
@@ -23,6 +23,11 @@ namespace MediaBrowser.Controller.Providers
/// true if this instance can refresh the specified item.
bool CanRefresh(BaseItem item);
+ ///
+ /// Determines whether this instance primarily targets the specified type.
+ ///
+ /// The type.
+ /// true if this instance primarily targets the specified type.
bool CanRefreshPrimary(Type type);
///
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 44bc4a50cb..32a7951f62 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -131,6 +131,24 @@ namespace MediaBrowser.Controller.Providers
/// IEnumerable{ImageProviderInfo}.
IEnumerable GetRemoteImageProviderInfo(BaseItem item);
+ ///
+ /// Gets the image providers for the provided item.
+ ///
+ /// The item.
+ /// The image refresh options.
+ /// The image providers for the item.
+ IEnumerable GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions);
+
+ ///
+ /// Gets the metadata providers for the provided item.
+ ///
+ /// The item.
+ /// The library options.
+ /// The type of metadata provider.
+ /// The metadata providers.
+ IEnumerable> GetMetadataProviders(BaseItem item, LibraryOptions libraryOptions)
+ where T : BaseItem;
+
///
/// Gets all metadata plugins.
///
diff --git a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs
index f146decb60..888ca6c72c 100644
--- a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs
+++ b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,20 +6,41 @@ using MediaBrowser.Model.Providers;
namespace MediaBrowser.Controller.Providers
{
+ ///
+ /// Interface IRemoteMetadataProvider.
+ ///
public interface IRemoteMetadataProvider : IMetadataProvider
{
}
+ ///
+ /// Interface IRemoteMetadataProvider.
+ ///
public interface IRemoteMetadataProvider : IMetadataProvider, IRemoteMetadataProvider, IRemoteSearchProvider
where TItemType : BaseItem, IHasLookupInfo
where TLookupInfoType : ItemLookupInfo, new()
{
+ ///
+ /// Gets the metadata for a specific LookupInfoType.
+ ///
+ /// The LookupInfoType to get metadata for.
+ /// The .
+ /// A task returning a MetadataResult for the specific LookupInfoType.
Task> GetMetadata(TLookupInfoType info, CancellationToken cancellationToken);
}
+ ///
+ /// Interface IRemoteMetadataProvider.
+ ///
public interface IRemoteSearchProvider : IRemoteSearchProvider
where TLookupInfoType : ItemLookupInfo
{
+ ///
+ /// Gets the list of for a specific LookupInfoType.
+ ///
+ /// The LookupInfoType to search for.
+ /// The .
+ /// A task returning RemoteSearchResults for the searchInfo.
Task> GetSearchResults(TLookupInfoType searchInfo, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index a9e1b4a51e..92ce14be23 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -68,7 +68,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
IgnoreComments = true
};
- _validProviderIds = _validProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _validProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase);
var idInfos = ProviderManager.GetExternalIdInfos(item.Item);
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 9b4b1db947..8c8fc6b0f8 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -102,7 +102,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
"tonemap_vaapi",
"procamp_vaapi",
"overlay_vaapi",
- "hwupload_vaapi"
+ "hwupload_vaapi",
+ // vulkan
+ "libplacebo",
+ "scale_vulkan",
+ "overlay_vulkan"
};
private static readonly IReadOnlyDictionary _filterOptionsDict = new Dictionary
@@ -111,7 +115,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
{ 2, new string[] { "tonemap_opencl", "bt2390" } },
{ 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } },
- { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } }
+ { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } },
+ { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } }
};
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
@@ -351,6 +356,39 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ public bool CheckVulkanDrmDeviceByExtensionName(string renderNodePath, string[] vulkanExtensions)
+ {
+ if (!OperatingSystem.IsLinux())
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(renderNodePath))
+ {
+ return false;
+ }
+
+ try
+ {
+ var command = "-v verbose -hide_banner -init_hw_device drm=dr:" + renderNodePath + " -init_hw_device vulkan=vk@dr";
+ var output = GetProcessOutput(_encoderPath, command, true, null);
+ foreach (string ext in vulkanExtensions)
+ {
+ if (!output.Contains(ext, StringComparison.Ordinal))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error detecting the given drm render node path");
+ return false;
+ }
+ }
+
private IEnumerable GetHwaccelTypes()
{
string? output = null;
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 757a01715a..b7c49ed99b 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -51,6 +51,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
private readonly IConfiguration _config;
+ private readonly IServerConfigurationManager _serverConfig;
private readonly string _startupOptionFFmpegPath;
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
@@ -72,6 +73,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false;
private bool _isVaapiDeviceInteli965 = false;
+ private bool _isVaapiDeviceSupportVulkanFmtModifier = false;
+
+ private static string[] _vulkanFmtModifierExts = {
+ "VK_KHR_sampler_ycbcr_conversion",
+ "VK_EXT_image_drm_format_modifier",
+ "VK_KHR_external_memory_fd",
+ "VK_EXT_external_memory_dma_buf",
+ "VK_KHR_external_semaphore_fd",
+ "VK_EXT_external_memory_host"
+ };
private Version _ffmpegVersion = null;
private string _ffmpegPath = string.Empty;
@@ -83,13 +94,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
ILocalizationManager localization,
- IConfiguration config)
+ IConfiguration config,
+ IServerConfigurationManager serverConfig)
{
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_localization = localization;
_config = config;
+ _serverConfig = serverConfig;
_startupOptionFFmpegPath = config.GetValue(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
_jsonSerializerOptions = JsonDefaults.Options;
}
@@ -110,6 +123,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
+ public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
+
///
/// Run at startup or if the user removes a Custom path from transcode page.
/// Sets global variables FFmpegPath.
@@ -169,6 +184,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
_isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
+ _isVaapiDeviceSupportVulkanFmtModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanFmtModifierExts);
+
if (_isVaapiDeviceAmd)
{
_logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
@@ -181,6 +198,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
}
+
+ if (_isVaapiDeviceSupportVulkanFmtModifier)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM format modifier", options.VaapiDevice);
+ }
}
}
@@ -587,6 +609,32 @@ namespace MediaBrowser.MediaEncoding.Encoder
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false);
}
+ private string GetImageResolutionParameter()
+ {
+ string imageResolutionParameter;
+
+ imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
+ {
+ ImageResolution.P144 => "256x144",
+ ImageResolution.P240 => "426x240",
+ ImageResolution.P360 => "640x360",
+ ImageResolution.P480 => "854x480",
+ ImageResolution.P720 => "1280x720",
+ ImageResolution.P1080 => "1920x1080",
+ ImageResolution.P1440 => "2560x1440",
+ ImageResolution.P2160 => "3840x2160",
+ _ => string.Empty
+ };
+
+ if (!string.IsNullOrEmpty(imageResolutionParameter))
+ {
+ imageResolutionParameter = " -s " + imageResolutionParameter;
+ }
+
+ return imageResolutionParameter;
+ }
+
+
private async Task ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
@@ -657,7 +705,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var vf = string.Join(',', filters);
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
- var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads);
+ var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, GetImageResolutionParameter());
if (offset.HasValue)
{
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index e61b896b9d..a07ab7121e 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Updates;
@@ -240,5 +241,23 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
///
public bool AllowClientLogUpload { get; set; } = true;
+
+ ///
+ /// Gets or sets the dummy chapters duration in seconds.
+ ///
+ /// The dummy chapters duration.
+ public int DummyChapterDuration { get; set; } = 300;
+
+ ///
+ /// Gets or sets the dummy chapter count.
+ ///
+ /// The dummy chapter count.
+ public int DummyChapterCount { get; set; } = 100;
+
+ ///
+ /// Gets or sets the chapter image resolution.
+ ///
+ /// The chapter image resolution.
+ public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index b121a29058..6e9b943f74 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -436,9 +436,9 @@ namespace MediaBrowser.Model.Dlna
{
containerSupported = true;
- videoSupported = videoStream != null && profile.SupportsVideoCodec(videoStream.Codec);
+ videoSupported = videoStream == null || profile.SupportsVideoCodec(videoStream.Codec);
- audioSupported = audioStream != null && profile.SupportsAudioCodec(audioStream.Codec);
+ audioSupported = audioStream == null || profile.SupportsAudioCodec(audioStream.Codec);
if (videoSupported && audioSupported)
{
@@ -447,18 +447,17 @@ namespace MediaBrowser.Model.Dlna
}
}
- var list = new List();
if (!containerSupported)
{
reasons |= TranscodeReason.ContainerNotSupported;
}
- if (videoStream != null && !videoSupported)
+ if (!videoSupported)
{
reasons |= TranscodeReason.VideoCodecNotSupported;
}
- if (audioStream != null && !audioSupported)
+ if (!audioSupported)
{
reasons |= TranscodeReason.AudioCodecNotSupported;
}
@@ -587,21 +586,19 @@ namespace MediaBrowser.Model.Dlna
}
// Collect candidate audio streams
- IEnumerable candidateAudioStreams = audioStream == null ? Array.Empty() : new[] { audioStream };
+ ICollection candidateAudioStreams = audioStream == null ? Array.Empty() : new[] { audioStream };
if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
{
if (audioStream?.IsDefault == true)
{
- candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault);
+ candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray();
}
else
{
- candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language);
+ candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language).ToArray();
}
}
- candidateAudioStreams = candidateAudioStreams.ToArray();
-
var videoStream = item.VideoStream;
var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
@@ -1057,7 +1054,7 @@ namespace MediaBrowser.Model.Dlna
MediaSourceInfo mediaSource,
MediaStream videoStream,
MediaStream audioStream,
- IEnumerable candidateAudioStreams,
+ ICollection candidateAudioStreams,
MediaStream subtitleStream,
bool isEligibleForDirectPlay,
bool isEligibleForDirectStream)
@@ -1088,9 +1085,6 @@ namespace MediaBrowser.Model.Dlna
bool? isInterlaced = videoStream?.IsInterlaced;
string videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
- // Audio
- var defaultLanguage = audioStream?.Language ?? string.Empty;
- var defaultMarked = audioStream?.IsDefault ?? false;
TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
int? packetLength = videoStream?.PacketLength;
@@ -1122,7 +1116,7 @@ namespace MediaBrowser.Model.Dlna
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
// Check audiocandidates profile conditions
- var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked));
+ var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream));
TranscodeReason subtitleProfileReasons = 0;
if (subtitleStream != null)
@@ -1179,14 +1173,18 @@ namespace MediaBrowser.Model.Dlna
}
// Check audio codec
- var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
- if (selectedAudioStream == null)
+ MediaStream selectedAudioStream = null;
+ if (candidateAudioStreams.Any())
{
- directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
- }
- else
- {
- audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
+ selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
+ if (selectedAudioStream == null)
+ {
+ directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
+ }
+ else
+ {
+ audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
+ }
}
var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
@@ -1239,10 +1237,10 @@ namespace MediaBrowser.Model.Dlna
return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
}
- private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string language, bool isDefault)
+ private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
{
var profile = options.Profile;
- var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, !audioStream.IsDefault);
+ var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions);
if (audioStream?.IsExternal == true)
diff --git a/MediaBrowser.Model/Drawing/ImageResolution.cs b/MediaBrowser.Model/Drawing/ImageResolution.cs
new file mode 100644
index 0000000000..34738b7990
--- /dev/null
+++ b/MediaBrowser.Model/Drawing/ImageResolution.cs
@@ -0,0 +1,52 @@
+namespace MediaBrowser.Model.Drawing;
+
+///
+/// Enum ImageResolution.
+///
+public enum ImageResolution
+{
+ ///
+ /// MatchSource.
+ ///
+ MatchSource = 0,
+
+ ///
+ /// 144p.
+ ///
+ P144 = 1,
+
+ ///
+ /// 240p.
+ ///
+ P240 = 2,
+
+ ///
+ /// 360p.
+ ///
+ P360 = 3,
+
+ ///
+ /// 480p.
+ ///
+ P480 = 4,
+
+ ///
+ /// 720p.
+ ///
+ P720 = 5,
+
+ ///
+ /// 1080p.
+ ///
+ P1080 = 6,
+
+ ///
+ /// 1440p.
+ ///
+ P1440 = 7,
+
+ ///
+ /// 2160p.
+ ///
+ P2160 = 8
+}
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index bb98488480..c348e83ae5 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -230,19 +230,15 @@ namespace MediaBrowser.Model.Dto
public bool? IsSecondaryAudio(MediaStream stream)
{
- // Look for the first audio track marked as default
- foreach (var currentStream in MediaStreams)
+ if (stream.IsExternal)
{
- if (currentStream.Type == MediaStreamType.Audio && currentStream.IsDefault)
- {
- return currentStream.Index != stream.Index;
- }
+ return false;
}
// Look for the first audio track
foreach (var currentStream in MediaStreams)
{
- if (currentStream.Type == MediaStreamType.Audio)
+ if (currentStream.Type == MediaStreamType.Audio && !currentStream.IsExternal)
{
return currentStream.Index != stream.Index;
}
diff --git a/MediaBrowser.Model/Entities/ExtraType.cs b/MediaBrowser.Model/Entities/ExtraType.cs
index aca4bd2829..66da80d96b 100644
--- a/MediaBrowser.Model/Entities/ExtraType.cs
+++ b/MediaBrowser.Model/Entities/ExtraType.cs
@@ -13,6 +13,8 @@ namespace MediaBrowser.Model.Entities
Scene = 6,
Sample = 7,
ThemeSong = 8,
- ThemeVideo = 9
+ ThemeVideo = 9,
+ Featurette = 10,
+ Short = 11
}
}
diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs
index 37e3d88645..bd8db99416 100644
--- a/MediaBrowser.Model/Entities/MetadataProvider.cs
+++ b/MediaBrowser.Model/Entities/MetadataProvider.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
namespace MediaBrowser.Model.Entities
{
///
@@ -14,38 +12,78 @@ namespace MediaBrowser.Model.Entities
Custom = 0,
///
- /// The imdb.
+ /// The IMDb provider.
///
Imdb = 2,
///
- /// The TMDB.
+ /// The TMDb provider.
///
Tmdb = 3,
///
- /// The TVDB.
+ /// The TVDb provider.
///
Tvdb = 4,
///
- /// The tvcom.
+ /// The tvcom providerd.
///
Tvcom = 5,
///
- /// Tmdb Collection Id.
+ /// TMDb collection provider.
///
TmdbCollection = 7,
+
+ ///
+ /// The MusicBrainz album provider.
+ ///
MusicBrainzAlbum = 8,
+
+ ///
+ /// The MusicBrainz album artist provider.
+ ///
MusicBrainzAlbumArtist = 9,
+
+ ///
+ /// The MusicBrainz artist provider.
+ ///
MusicBrainzArtist = 10,
+
+ ///
+ /// The MusicBrainz release group provider.
+ ///
MusicBrainzReleaseGroup = 11,
+
+ ///
+ /// The Zap2It provider.
+ ///
Zap2It = 12,
+
+ ///
+ /// The TvRage provider.
+ ///
TvRage = 15,
+
+ ///
+ /// The AudioDb artist provider.
+ ///
AudioDbArtist = 16,
+
+ ///
+ /// The AudioDb collection provider.
+ ///
AudioDbAlbum = 17,
+
+ ///
+ /// The MusicBrainz track provider.
+ ///
MusicBrainzTrack = 18,
+
+ ///
+ /// The TvMaze provider.
+ ///
TvMaze = 19
}
}
diff --git a/MediaBrowser.Model/Entities/SeriesStatus.cs b/MediaBrowser.Model/Entities/SeriesStatus.cs
index c77c4a8ad9..1cff24e2a6 100644
--- a/MediaBrowser.Model/Entities/SeriesStatus.cs
+++ b/MediaBrowser.Model/Entities/SeriesStatus.cs
@@ -1,18 +1,23 @@
namespace MediaBrowser.Model.Entities
{
///
- /// Enum SeriesStatus.
+ /// The status of a series.
///
public enum SeriesStatus
{
///
- /// The continuing.
+ /// The continuing status. This indicates that a series is currently releasing.
///
Continuing,
///
- /// The ended.
+ /// The ended status. This indicates that a series has completed and is no longer being released.
///
- Ended
+ Ended,
+
+ ///
+ /// The unreleased status. This indicates that a series has not been released yet.
+ ///
+ Unreleased
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index ad2ff1ba29..4172e9825e 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -34,13 +34,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
index e6c3a6c260..6fa1d778ad 100644
--- a/MediaBrowser.Model/Querying/ItemFields.cs
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -126,7 +126,7 @@ namespace MediaBrowser.Model.Querying
ProductionLocations,
///
- /// Imdb, tmdb, etc.
+ /// The ids from IMDb, TMDb, etc.
///
ProviderIds,
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 5a2936bd8b..0f3f1bdcba 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -94,7 +94,7 @@ namespace MediaBrowser.Providers.Manager
var localImagesFailed = false;
- var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList();
+ var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
{
@@ -522,7 +522,7 @@ namespace MediaBrowser.Providers.Manager
protected IEnumerable GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefreshOptions options, bool isFirstRefresh, bool requiresRefresh)
{
// Get providers to refresh
- var providers = ((ProviderManager)ProviderManager).GetMetadataProviders(item, libraryOptions).ToList();
+ var providers = ProviderManager.GetMetadataProviders(item, libraryOptions).ToList();
var metadataRefreshMode = options.MetadataRefreshMode;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index bbb33ddf0e..ac4dc1bc31 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -48,7 +46,7 @@ namespace MediaBrowser.Providers.Manager
///
public class ProviderManager : IProviderManager, IDisposable
{
- private readonly object _refreshQueueLock = new object();
+ private readonly object _refreshQueueLock = new();
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryMonitor _libraryMonitor;
@@ -58,11 +56,11 @@ namespace MediaBrowser.Providers.Manager
private readonly ISubtitleManager _subtitleManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IBaseItemManager _baseItemManager;
- private readonly ConcurrentDictionary _activeRefreshes = new ConcurrentDictionary();
- private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
- private readonly SimplePriorityQueue> _refreshQueue =
- new SimplePriorityQueue>();
+ private readonly ConcurrentDictionary _activeRefreshes = new();
+ private readonly CancellationTokenSource _disposeCancellationTokenSource = new();
+ private readonly SimplePriorityQueue> _refreshQueue = new();
+ private IImageProvider[] _imageProviders = Array.Empty();
private IMetadataService[] _metadataServices = Array.Empty();
private IMetadataProvider[] _metadataProviders = Array.Empty();
private IMetadataSaver[] _savers = Array.Empty();
@@ -105,15 +103,13 @@ namespace MediaBrowser.Providers.Manager
}
///
- public event EventHandler> RefreshStarted;
+ public event EventHandler>? RefreshStarted;
///
- public event EventHandler> RefreshCompleted;
+ public event EventHandler>? RefreshCompleted;
///