Merge branch 'master' into fix-fmp4-flac-opus
This commit is contained in:
commit
4e34c428d8
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
|
@ -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
|
6
.github/renovate.json
vendored
Normal file
6
.github/renovate.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>jellyfin/.github//renovate-presets/dotnet"
|
||||
]
|
||||
}
|
12
.github/workflows/automation.yml
vendored
12
.github/workflows/automation.yml
vendored
|
@ -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:
|
||||
|
|
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
|
@ -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
|
||||
|
|
16
.github/workflows/commands.yml
vendored
16
.github/workflows/commands.yml
vendored
|
@ -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 }}
|
||||
|
|
22
.github/workflows/openapi.yml
vendored
22
.github/workflows/openapi.yml
vendored
|
@ -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:
|
|||
|
||||
</details>
|
||||
- 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 }}
|
||||
|
|
2
.github/workflows/repo-stale.yaml
vendored
2
.github/workflows/repo-stale.yaml
vendored
|
@ -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
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="$(SolutionDir)/BannedSymbols.txt" />
|
||||
<AdditionalFiles Include="$(SolutionDir)/stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -127,8 +127,7 @@ namespace Emby.Dlna.Eventing
|
|||
public Task TriggerEvent(string notificationType, IDictionary<string, string> 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));
|
||||
|
||||
|
|
|
@ -338,7 +338,6 @@ namespace Emby.Dlna.PlayTo
|
|||
SubtitleStreamIndex = info.SubtitleStreamIndex,
|
||||
VolumeLevel = _device.Volume,
|
||||
|
||||
// TODO
|
||||
CanSeek = true,
|
||||
|
||||
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
|
||||
|
|
|
@ -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<AudioBookFileInfo>()
|
||||
.ToList();
|
||||
.OfType<AudioBookFileInfo>();
|
||||
|
||||
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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
|
|||
/// <returns>A task representing the service initialization operation.</returns>
|
||||
public async Task InitializeServices()
|
||||
{
|
||||
var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDb>>().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<ILocalizationManager>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -25,13 +25,13 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.4" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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<Folder>()
|
||||
.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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -2192,16 +2192,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
private void HandleDuplicateShowIds(List<TimerInfo> 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<TimerInfo> timers)
|
||||
private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> 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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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<ImageDataDto> images, double desiredAspect)
|
||||
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> 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<string> programIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
|
@ -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
|
||||
{
|
||||
|
|
|
@ -32,18 +32,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<XmlTvListingsProvider> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public XmlTvListingsProvider(
|
||||
IServerConfigurationManager config,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<XmlTvListingsProvider> logger,
|
||||
IFileSystem fileSystem)
|
||||
ILogger<XmlTvListingsProvider> 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))
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
"TasksChannelsCategory": "قنوات الإنترنت",
|
||||
"TasksLibraryCategory": "مكتبة",
|
||||
"TasksMaintenanceCategory": "صيانة",
|
||||
"TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
|
||||
"TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
|
||||
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
|
||||
"TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
|
||||
"TaskRefreshChapterImages": "استخراج صور الفصل",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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í"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -123,5 +123,6 @@
|
|||
"TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων",
|
||||
"TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
|
||||
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
|
||||
"External": "Εξωτερικό"
|
||||
"External": "Εξωτερικό",
|
||||
"HearingImpaired": "Με προβλήματα ακοής"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -123,5 +123,6 @@
|
|||
"TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים ומוריד את שטח האחסון שבשימוש. הרצה של פעולה זו לאחר סריקת הספרייה או שינויים אחרים שמשפיעים על מסד הנתונים יכולה לשפר ביצועים.",
|
||||
"TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.",
|
||||
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
|
||||
"External": "חיצוני"
|
||||
"External": "חיצוני",
|
||||
"HearingImpaired": "לקוי שמיעה"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
7
Emby.Server.Implementations/Localization/Core/jbo.json
Normal file
7
Emby.Server.Implementations/Localization/Core/jbo.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"Albums": "lo albuma",
|
||||
"Artists": "lo larpra",
|
||||
"Books": "lo cukta",
|
||||
"HeaderAlbumArtists": "lo albuma larpra",
|
||||
"Playlists": "lo zgipor"
|
||||
}
|
3
Emby.Server.Implementations/Localization/Core/km.json
Normal file
3
Emby.Server.Implementations/Localization/Core/km.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"Albums": "Albums"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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ă"
|
||||
}
|
||||
|
|
|
@ -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": "Для слабослышащих"
|
||||
}
|
||||
|
|
|
@ -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ý"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -122,5 +122,6 @@
|
|||
"TaskOptimizeDatabaseDescription": "Стискає базу даних та збільшує вільний простір. Виконання цього завдання після сканування медіатеки або внесення інших змін, які передбачають модифікацію бази даних може покращити продуктивність.",
|
||||
"TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.",
|
||||
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
|
||||
"External": "Зовнішній"
|
||||
"External": "Зовнішній",
|
||||
"HearingImpaired": "З порушеннями слуху"
|
||||
}
|
||||
|
|
|
@ -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": "گانے",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -123,5 +123,6 @@
|
|||
"TaskOptimizeDatabase": "优化数据库",
|
||||
"TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
|
||||
"TaskKeyframeExtractor": "关键帧提取器",
|
||||
"External": "外部"
|
||||
"External": "外部",
|
||||
"HearingImpaired": "听力障碍"
|
||||
}
|
||||
|
|
|
@ -123,5 +123,6 @@
|
|||
"TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。",
|
||||
"TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。",
|
||||
"TaskKeyframeExtractor": "關鍵幀提取器",
|
||||
"External": "外部"
|
||||
"External": "外部",
|
||||
"HearingImpaired": "聽力障礙"
|
||||
}
|
||||
|
|
|
@ -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": "聽力障礙"
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|||
/// </summary>
|
||||
public class ChapterImagesTask : IScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
private readonly ILogger<ChapterImagesTask> _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
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>.
|
||||
/// <param name="libraryManager">The library manager.</param>.
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
|
@ -47,6 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||
/// <param name="fileSystem">The filesystem.</param>
|
||||
/// <param name="localization">The localization manager.</param>
|
||||
public ChapterImagesTask(
|
||||
ILogger<ChapterImagesTask> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||
{
|
||||
private readonly ILogger<OptimizeDatabaseTask> _logger;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly JellyfinDbProvider _provider;
|
||||
private readonly IDbContextFactory<JellyfinDb> _provider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
|
||||
|
@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||
public OptimizeDatabaseTask(
|
||||
ILogger<OptimizeDatabaseTask> logger,
|
||||
ILocalizationManager localization,
|
||||
JellyfinDbProvider provider)
|
||||
IDbContextFactory<JellyfinDb> provider)
|
||||
{
|
||||
_logger = logger;
|
||||
_localization = localization;
|
||||
|
@ -70,30 +70,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
public async Task ExecuteAsync(IProgress<double> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Episode>().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<Episode>().ElementAtOrDefault(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||
nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
|
||||
}
|
||||
|
||||
var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||
{
|
||||
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
|
|
|
@ -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<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
|
||||
{
|
||||
type = order < 8 ? defaults[order] : HomeSectionType.None;
|
||||
|
|
|
@ -87,9 +87,9 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
|
||||
/// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
|
||||
/// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
|
@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">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.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
|
@ -282,39 +282,13 @@ namespace Jellyfin.Api.Controllers
|
|||
includeItemTypes = new[] { BaseItemKind.Playlist };
|
||||
}
|
||||
|
||||
var enabledChannels = isApiKey
|
||||
? Array.Empty<Guid>()
|
||||
: user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
|
||||
|
||||
// api keys are always enabled for all folders
|
||||
bool isInEnabledFolder = isApiKey
|
||||
|| Array.IndexOf(user!.GetPreferenceValues<Guid>(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<Guid>(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
|
|||
/// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
|
||||
/// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
|
||||
/// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
|
@ -575,7 +549,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">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.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
|
|
|
@ -485,7 +485,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="200">Media folders returned.</response>
|
||||
/// <returns>List of user media folders.</returns>
|
||||
[HttpGet("Library/MediaFolders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -55,9 +55,9 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
|
||||
/// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
|
||||
/// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
|
@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">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.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
|
||||
|
|
|
@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity
|
|||
/// </summary>
|
||||
public class ActivityManager : IActivityManager
|
||||
{
|
||||
private readonly JellyfinDbProvider _provider;
|
||||
private readonly IDbContextFactory<JellyfinDb> _provider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="provider">The Jellyfin database provider.</param>
|
||||
public ActivityManager(JellyfinDbProvider provider)
|
||||
public ActivityManager(IDbContextFactory<JellyfinDb> provider)
|
||||
{
|
||||
_provider = provider;
|
||||
}
|
||||
|
@ -32,10 +32,12 @@ namespace Jellyfin.Server.Implementations.Activity
|
|||
/// <inheritdoc/>
|
||||
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<ActivityLogEntry>(ConvertToOldModel(entry)));
|
||||
}
|
||||
|
@ -43,44 +45,47 @@ namespace Jellyfin.Server.Implementations.Activity
|
|||
/// <inheritdoc/>
|
||||
public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
|
||||
{
|
||||
await using var dbContext = _provider.CreateContext();
|
||||
|
||||
IQueryable<ActivityLog> 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<ActivityLog> 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<ActivityLogEntry>(
|
||||
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<ActivityLogEntry>(
|
||||
query.Skip,
|
||||
await entries.CountAsync().ConfigureAwait(false),
|
||||
await entries
|
||||
.Skip(query.Skip ?? 0)
|
||||
.Take(query.Limit ?? 100)
|
||||
.AsAsyncEnumerable()
|
||||
.Select(ConvertToOldModel)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
|
|
|
@ -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
|
|||
/// </summary>
|
||||
public class DeviceManager : IDeviceManager
|
||||
{
|
||||
private readonly JellyfinDbProvider _dbProvider;
|
||||
private readonly IDbContextFactory<JellyfinDb> _dbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
||||
|
||||
|
@ -31,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||
/// </summary>
|
||||
/// <param name="dbProvider">The database provider.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager)
|
||||
public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager)
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
_userManager = userManager;
|
||||
|
@ -49,16 +50,20 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||
/// <inheritdoc />
|
||||
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<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
||||
}
|
||||
|
@ -66,22 +71,29 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||
/// <inheritdoc />
|
||||
public async Task<Device> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeviceOptions> 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
|
|||
/// <inheritdoc />
|
||||
public async Task<DeviceInfo?> 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
|
|||
/// <inheritdoc />
|
||||
public async Task<QueryResult<Device>> 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<Device>(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<Device>(
|
||||
query.Skip,
|
||||
count,
|
||||
await devices.ToListAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -165,37 +179,43 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||
/// <inheritdoc />
|
||||
public async Task<QueryResult<DeviceInfo>> 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<Device> 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<DeviceInfo>(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<DeviceInfo>(array);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="IServiceCollection"/> interface.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
||||
/// </summary>
|
||||
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
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<JellyfinDb>((serviceProvider, opt) =>
|
||||
{
|
||||
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
|
||||
.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>())
|
||||
.UseLoggerFactory(loggerFactory);
|
||||
});
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
}
|
|
@ -26,14 +26,15 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.7.5" />
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory class for generating new <see cref="JellyfinDb"/> instances.
|
||||
/// </summary>
|
||||
public class JellyfinDbProvider
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly ILogger<JellyfinDbProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The application's service provider.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger<JellyfinDbProvider> 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="JellyfinDb"/> context.
|
||||
/// </summary>
|
||||
/// <returns>The newly created context.</returns>
|
||||
public JellyfinDb CreateContext()
|
||||
{
|
||||
var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
|
||||
return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options);
|
||||
}
|
||||
}
|
||||
}
|
657
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
generated
Normal file
657
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
generated
Normal file
|
@ -0,0 +1,657 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
// <auto-generated />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DayOfWeek")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("EndHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("StartHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AccessSchedules", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LogSeverity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShortOverview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DateCreated");
|
||||
|
||||
b.ToTable("ActivityLogs", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChromecastVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DashboardTheme")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableNextVideoInfoOverlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ScrollDirection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowBackdrop")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowSidebar")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipBackwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipForwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TvHome")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DisplayPreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayPreferencesId");
|
||||
|
||||
b.ToTable("HomeSection", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ImageInfos", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("RememberIndexing")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSorting")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SortBy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ViewType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Permission_Permissions_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Value")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Permissions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Preference_Preferences_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Preferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastActivity")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccessToken")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ApiKeys", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastActivity")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("AccessToken", "DateLastActivity");
|
||||
|
||||
b.HasIndex("DeviceId", "DateLastActivity");
|
||||
|
||||
b.HasIndex("UserId", "DeviceId");
|
||||
|
||||
b.ToTable("Devices", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceOptions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AudioLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AuthenticationProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DisplayCollectionsView")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("DisplayMissingEpisodes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EasyPassword")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableAutoLogin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableLocalPassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableNextEpisodeAutoPlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableUserPreferenceAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HidePlayedInLatest")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InternalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("InvalidLoginAttemptCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastActivityDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLoginDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LoginAttemptsBeforeLockout")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxActiveSessions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalAgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("MustUpdatePassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordResetProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PlayDefaultAudioTrack")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberAudioSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSubtitleSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("RemoteClientBitrateLimit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SubtitleLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SubtitleMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SyncPlayAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 =>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// FluentAPI configuration for the ActivityLog entity.
|
||||
/// </summary>
|
||||
public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<ActivityLog> builder)
|
||||
{
|
||||
builder.HasIndex(entity => entity.DateCreated);
|
||||
}
|
||||
}
|
|
@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security
|
|||
/// <inheritdoc />
|
||||
public class AuthenticationManager : IAuthenticationManager
|
||||
{
|
||||
private readonly JellyfinDbProvider _dbProvider;
|
||||
private readonly IDbContextFactory<JellyfinDb> _dbProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbProvider">The database provider.</param>
|
||||
public AuthenticationManager(JellyfinDbProvider dbProvider)
|
||||
public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider)
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
}
|
||||
|
@ -24,50 +24,56 @@ namespace Jellyfin.Server.Implementations.Security
|
|||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AuthenticationInfo>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<JellyfinDb> _jellyfinDbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerApplicationHost _serverApplicationHost;
|
||||
|
||||
public AuthorizationContext(
|
||||
JellyfinDbProvider jellyfinDb,
|
||||
IDbContextFactory<JellyfinDb> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -20,10 +20,10 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context.</param>
|
||||
public DisplayPreferencesManager(JellyfinDb dbContext)
|
||||
/// <param name="dbContextFactory">The database context factory.</param>
|
||||
public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_dbContext = dbContextFactory.CreateDbContext();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// </summary>
|
||||
public class UserManager : IUserManager
|
||||
{
|
||||
private readonly JellyfinDbProvider _dbProvider;
|
||||
private readonly IDbContextFactory<JellyfinDb> _dbProvider;
|
||||
private readonly IEventManager _eventManager;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public UserManager(
|
||||
JellyfinDbProvider dbProvider,
|
||||
IDbContextFactory<JellyfinDb> dbProvider,
|
||||
IEventManager eventManager,
|
||||
ICryptoProvider cryptoProvider,
|
||||
INetworkManager networkManager,
|
||||
|
@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||
|
||||
_users = new ConcurrentDictionary<Guid, User>();
|
||||
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>(user));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<User> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -584,105 +598,111 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<UnratedItem>());
|
||||
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<UnratedItem>());
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<JellyfinDb>(
|
||||
options => options
|
||||
.UseLoggerFactory(LoggerFactory)
|
||||
.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
|
||||
|
||||
serviceCollection.AddEventServices();
|
||||
serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
|
||||
serviceCollection.AddSingleton<IEventManager, EventManager>();
|
||||
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
||||
|
||||
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
|
||||
serviceCollection.AddSingleton<IUserManager, UserManager>();
|
||||
serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
|
||||
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
|
||||
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
|
||||
|
||||
// TODO search the assemblies instead of adding them manually?
|
||||
|
|
|
@ -434,11 +434,15 @@ namespace Jellyfin.Server.Extensions
|
|||
options.MapType<TranscodeReason>(() =>
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Enum = Enum.GetNames<TranscodeReason>()
|
||||
.Select(e => new OpenApiString(e))
|
||||
.Cast<IOpenApiAny>()
|
||||
.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.
|
||||
|
|
|
@ -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<TranscodeReason>()
|
||||
.Select(e => new OpenApiString(e))
|
||||
.Cast<IOpenApiAny>()
|
||||
.ToArray()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
|
||||
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.11" />
|
||||
<PackageReference Include="prometheus-net" Version="7.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user