Merge branch 'jellyfin:master' into Bugfix-wrong-album-artwork

pull/11227/head
MisterGenius 4 weeks ago committed by GitHub
commit 93467b4402
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.3",
"version": "8.0.5",
"commands": [
"dotnet-ef"
]

@ -38,10 +38,11 @@ body:
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- 10.9.0
- 10.8.13
- 10.8.12
- 10.8.11 or older (please specify)
- Unstable (master branch)
- 10.8.12 or older (please specify)
- Weekly unstable (please specify)
- Master branch
validations:
required: true
- type: input

@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup .NET
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
uses: github/codeql-action/init@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
uses: github/codeql-action/autobuild@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
uses: github/codeql-action/analyze@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5

@ -3,6 +3,8 @@ on:
push:
branches:
- master
tags:
- 'v*'
pull_request_target:
permissions: {}
@ -14,7 +16,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -25,7 +27,7 @@ jobs:
- 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: openapi-head
retention-days: 14
@ -39,7 +41,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -59,7 +61,7 @@ jobs:
- 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: openapi-base
retention-days: 14
@ -78,12 +80,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-base
path: openapi-base
@ -105,7 +107,7 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3.0.0
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
@ -138,10 +140,11 @@ jobs:
No changes to OpenAPI specification found. See history of this comment for previous changes.
publish:
publish-unstable:
name: OpenAPI - Publish Unstable Spec
if: |
github.event_name != 'pull_request_target' &&
${{ ! startsWith(github.ref, 'refs/tags/v') }} &&
contains(github.repository_owner, 'jellyfin')
runs-on: ubuntu-latest
needs:
@ -152,7 +155,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
@ -174,6 +177,12 @@ jobs:
debug: false
script_stop: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
@ -187,10 +196,75 @@ jobs:
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Create new jellyfin-openapi-stable.json symlink
# Create new jellyfin-openapi-unstable.json symlink
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
# Check that the previous openapi spec is correct
# Check that the previous openapi unstable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi
) 200>/run/workflows/openapi-unstable.lock
publish-stable:
name: OpenAPI - Publish Stable Spec
if: |
startsWith(github.ref, 'refs/tags/v') &&
contains(github.repository_owner, 'jellyfin')
runs-on: ubuntu-latest
needs:
- openapi-head
steps:
- name: Set version number
id: version
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script_stop: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-stable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Create new jellyfin-openapi-stable.json symlink
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
# Check that the previous openapi stable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
fi
) 200>/run/workflows/openapi-stable.lock

@ -19,7 +19,7 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@3e39bd1b454c2bac14560547e4394f9317672705 # 5.2.4
uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"

@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@ -128,11 +128,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: '3.12'
cache: 'pip'

@ -10,7 +10,7 @@ jobs:
issues: write
steps:
- name: pull in script
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
repository: jellyfin/jellyfin-triage-script
- name: install python

@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'

@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
ref: ${{ env.TAG_BRANCH }}
@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
ref: ${{ env.TAG_BRANCH }}

@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="6.3.4" />
<PackageVersion Include="AsyncKeyedLock" Version="6.4.2" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@ -13,27 +13,26 @@
<PackageVersion Include="BlurHashSharp" Version="1.3.2" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="3.3.27" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.3" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="libse" Version="4.0.5" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.5" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
@ -42,8 +41,8 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.5" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
@ -68,12 +67,12 @@
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
<PackageVersion Include="SkiaSharp" Version="2.88.7" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.7" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="1.0.0.16" />
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
@ -85,8 +84,8 @@
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.7.0" />
<PackageVersion Include="xunit" Version="2.7.1" />
</ItemGroup>
</Project>

@ -537,6 +537,12 @@ namespace Emby.Naming.Common
"extras",
MediaType.Video),
new ExtraRule(
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extra",
MediaType.Video),
new ExtraRule(
ExtraType.Unknown,
ExtraRuleType.DirectoryName,

@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.9.0</VersionPrefix>
<VersionPrefix>10.10.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

@ -127,15 +127,11 @@ namespace Emby.Server.Implementations.AppBase
if (_configurationFactories is null)
{
_configurationFactories = new[] { factory };
_configurationFactories = [factory];
}
else
{
var oldLen = _configurationFactories.Length;
var arr = new IConfigurationFactory[oldLen + 1];
_configurationFactories.CopyTo(arr, 0);
arr[oldLen] = factory;
_configurationFactories = arr;
_configurationFactories = [.._configurationFactories, factory];
}
_configurationStores = _configurationFactories

@ -109,13 +109,13 @@ namespace Emby.Server.Implementations
/// <summary>
/// The disposable parts.
/// </summary>
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly ConcurrentBag<IDisposable> _disposableParts = new();
private readonly DeviceId _deviceId;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private readonly PluginManager _pluginManager;
private List<Type> _creatingInstances;
@ -161,7 +161,7 @@ namespace Emby.Server.Implementations
ApplicationPaths.PluginsPath,
ApplicationVersion);
_disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
_disposableParts.Add(_pluginManager);
}
/// <summary>
@ -360,7 +360,7 @@ namespace Emby.Server.Implementations
{
foreach (var part in parts.OfType<IDisposable>())
{
_disposableParts.TryAdd(part, byte.MinValue);
_disposableParts.Add(part);
}
}
@ -381,7 +381,7 @@ namespace Emby.Server.Implementations
{
foreach (var part in parts.OfType<IDisposable>())
{
_disposableParts.TryAdd(part, byte.MinValue);
_disposableParts.Add(part);
}
}
@ -422,7 +422,7 @@ namespace Emby.Server.Implementations
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
_disposableParts.Add(DotNetRuntimeStatsBuilder.Default().StartCollecting());
}
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@ -457,7 +457,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IApplicationHost>(this);
serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
@ -965,7 +965,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Disposing {Type}", type.Name);
foreach (var (part, _) in _disposableParts)
foreach (var part in _disposableParts.ToArray())
{
var partType = part.GetType();
if (partType == type)

@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections");
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First();
}

@ -186,10 +186,7 @@ namespace Emby.Server.Implementations.Data
protected void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed.");
}
ObjectDisposedException.ThrowIf(_disposed, this);
}
/// <inheritdoc />

@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data
private const string SaveItemCommandText =
@"replace into TypedBaseItems
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
@ -111,6 +111,7 @@ namespace Emby.Server.Implementations.Data
"DateLastMediaAdded",
"Album",
"LUFS",
"NormalizationGain",
"CriticRating",
"IsVirtualItem",
"SeriesName",
@ -205,7 +206,7 @@ namespace Emby.Server.Implementations.Data
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
private static readonly string _mediaAttachmentInsertPrefix;
private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix();
private static readonly BaseItemKind[] _programTypes = new[]
{
@ -296,21 +297,6 @@ namespace Emby.Server.Implementations.Data
{ BaseItemKind.Year, typeof(Year).FullName }
};
static SqliteItemRepository()
{
var queryPrefixText = new StringBuilder();
queryPrefixText.Append("insert into mediaattachments (");
foreach (var column in _mediaAttachmentSaveColumns)
{
queryPrefixText.Append(column)
.Append(',');
}
queryPrefixText.Length -= 1;
queryPrefixText.Append(") values ");
_mediaAttachmentInsertPrefix = queryPrefixText.ToString();
}
/// <summary>
/// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
/// </summary>
@ -493,6 +479,7 @@ namespace Emby.Server.Implementations.Data
AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
@ -901,6 +888,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@Album", item.Album);
saveItemStatement.TryBind("@LUFS", item.LUFS);
saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
if (item is IHasSeries hasSeriesName)
@ -1687,6 +1675,11 @@ namespace Emby.Server.Implementations.Data
item.LUFS = lUFS;
}
if (reader.TryGetSingle(index++, out var normalizationGain))
{
item.NormalizationGain = normalizationGain;
}
if (reader.TryGetSingle(index++, out var criticRating))
{
item.CriticRating = criticRating;
@ -2330,14 +2323,7 @@ namespace Emby.Server.Implementations.Data
columns.Add(builder.ToString());
var oldLen = query.ExcludeItemIds.Length;
var newLen = oldLen + item.ExtraIds.Length + 1;
var excludeIds = new Guid[newLen];
query.ExcludeItemIds.CopyTo(excludeIds, 0);
excludeIds[oldLen] = item.Id;
item.ExtraIds.CopyTo(excludeIds, oldLen + 1);
query.ExcludeItemIds = excludeIds;
query.ExcludeItemIds = [..query.ExcludeItemIds, item.Id, ..item.ExtraIds];
query.ExcludeProviderIds = item.ProviderIds;
}
@ -2845,10 +2831,7 @@ namespace Emby.Server.Implementations.Data
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
}
var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
prepend.CopyTo(arr, 0);
orderBy.CopyTo(arr, prepend.Count);
orderBy = query.OrderBy = arr;
orderBy = query.OrderBy = [..prepend, ..orderBy];
}
else if (orderBy.Count == 0)
{
@ -4209,7 +4192,19 @@ namespace Emby.Server.Implementations.Data
{
int index = 0;
string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
// Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
// In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
{
whereClauses.Add($"""
((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
""");
}
else
{
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
}
}
else
{
@ -5879,6 +5874,21 @@ AND Type = @InternalPersonType)");
return item;
}
private static string BuildMediaAttachmentInsertPrefix()
{
var queryPrefixText = new StringBuilder();
queryPrefixText.Append("insert into mediaattachments (");
foreach (var column in _mediaAttachmentSaveColumns)
{
queryPrefixText.Append(column)
.Append(',');
}
queryPrefixText.Length -= 1;
queryPrefixText.Append(") values ");
return queryPrefixText.ToString();
}
#nullable enable
private readonly struct QueryTimeLogger : IDisposable

@ -58,7 +58,8 @@ namespace Emby.Server.Implementations.Data
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
"create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
if (!userDataTableExists)
{

@ -668,12 +668,13 @@ namespace Emby.Server.Implementations.Dto
{
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
if (!dto.ImageBlurHashes.ContainsKey(image.Type))
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
{
dto.ImageBlurHashes[image.Type] = new Dictionary<string, string>();
value = new Dictionary<string, string>();
dto.ImageBlurHashes[image.Type] = value;
}
dto.ImageBlurHashes[image.Type][tag] = image.BlurHash;
value[tag] = image.BlurHash;
}
return tag;
@ -897,16 +898,21 @@ namespace Emby.Server.Implementations.Dto
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
}
dto.LUFS = item.LUFS;
if (item.LUFS.HasValue)
{
// -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
dto.NormalizationGain = -18f - item.LUFS;
}
else if (item.NormalizationGain.HasValue)
{
dto.NormalizationGain = item.NormalizationGain;
}
// Add audio info
if (item is Audio audio)
{
dto.Album = audio.Album;
if (audio.ExtraType.HasValue)
{
dto.ExtraType = audio.ExtraType.Value.ToString();
}
dto.ExtraType = audio.ExtraType;
var albumParent = audio.AlbumEntity;
@ -1058,10 +1064,7 @@ namespace Emby.Server.Implementations.Dto
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
}
if (video.ExtraType.HasValue)
{
dto.ExtraType = video.ExtraType.Value.ToString();
}
dto.ExtraType = video.ExtraType;
}
if (options.ContainsField(ItemFields.MediaStreams))

@ -101,14 +101,14 @@ namespace Emby.Server.Implementations.HttpServer
var pipe = new Pipe();
var writer = pipe.Writer;
ValueWebSocketReceiveResult receiveresult;
ValueWebSocketReceiveResult receiveResult;
do
{
// Allocate at least 512 bytes from the PipeWriter
Memory<byte> memory = writer.GetMemory(512);
try
{
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
receiveResult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
}
catch (WebSocketException ex)
{
@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.HttpServer
break;
}
int bytesRead = receiveresult.Count;
int bytesRead = receiveResult.Count;
if (bytesRead == 0)
{
break;
@ -135,13 +135,13 @@ namespace Emby.Server.Implementations.HttpServer
LastActivityDate = DateTime.UtcNow;
if (receiveresult.EndOfMessage)
if (receiveResult.EndOfMessage)
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
}
while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
&& receiveResult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
@ -199,13 +199,20 @@ namespace Emby.Server.Implementations.HttpServer
}
else
{
await OnReceive(
new WebSocketMessageInfo
{
MessageType = stub.MessageType,
Data = stub.Data?.ToString(), // Data can be null
Connection = this
}).ConfigureAwait(false);
try
{
await OnReceive(
new WebSocketMessageInfo
{
MessageType = stub.MessageType,
Data = stub.Data?.ToString(), // Data can be null
Connection = this
}).ConfigureAwait(false);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to process WebSocket message");
}
}
}

@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
using var connection = new WebSocketConnection(
var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
authorizationInfo,
@ -56,17 +56,19 @@ namespace Emby.Server.Implementations.HttpServer
{
OnReceive = ProcessWebSocketMessageReceived
};
var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i)
await using (connection.ConfigureAwait(false))
{
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
}
var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i)
{
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
}
await Task.WhenAll(tasks).ConfigureAwait(false);
await Task.WhenAll(tasks).ConfigureAwait(false);
await connection.ReceiveAsync().ConfigureAwait(false);
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
await connection.ReceiveAsync().ConfigureAwait(false);
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
}
}
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
{

@ -80,12 +80,14 @@ namespace Emby.Server.Implementations.IO
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
// path is actually a stream
if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal))
if (string.IsNullOrWhiteSpace(filePath))
{
return filePath;
}
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
var isAbsolutePath = Path.IsPathRooted(filePath) && (!OperatingSystem.IsWindows() || filePath[0] != '\\');
if (isAbsolutePath)
{
// absolute local path
return filePath;
@ -97,17 +99,10 @@ namespace Emby.Server.Implementations.IO
return filePath;
}
var firstChar = filePath[0];
if (firstChar == '/')
{
// for this we don't really know
return filePath;
}
var filePathSpan = filePath.AsSpan();
// relative path
if (firstChar == '\\')
// relative path on windows
if (filePath[0] == '\\')
{
filePathSpan = filePathSpan.Slice(1);
}

@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
{
// Don't ignore application folders
if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))

@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
}
};
private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
private static readonly Glob[] _globs = Array.ConvertAll(_patterns, p => Glob.Parse(p, _globOptions));
/// <summary>
/// Returns true if the supplied path should be ignored.

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -89,8 +87,8 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// The _root folder.
/// </summary>
private volatile AggregateFolder _rootFolder;
private volatile UserRootFolder _userRootFolder;
private volatile AggregateFolder? _rootFolder;
private volatile UserRootFolder? _userRootFolder;
private bool _wizardCompleted;
@ -155,17 +153,17 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Occurs when [item added].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemAdded;
public event EventHandler<ItemChangeEventArgs>? ItemAdded;
/// <summary>
/// Occurs when [item updated].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
/// <summary>
/// Occurs when [item removed].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
/// <summary>
/// Gets the root folder.
@ -264,7 +262,7 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
private void ConfigurationUpdated(object sender, EventArgs e)
private void ConfigurationUpdated(object? sender, EventArgs e)
{
var config = _configurationManager.Configuration;
@ -338,7 +336,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram)
{
_logger.LogDebug(
"Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
"Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@ -347,7 +345,7 @@ namespace Emby.Server.Implementations.Library
else
{
_logger.LogInformation(
"Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
"Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@ -366,7 +364,7 @@ namespace Emby.Server.Implementations.Library
}
_logger.LogDebug(
"Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
"Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
@ -395,7 +393,7 @@ namespace Emby.Server.Implementations.Library
try
{
_logger.LogInformation(
"Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
"Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
@ -410,6 +408,24 @@ namespace Emby.Server.Implementations.Library
File.Delete(fileSystemInfo.FullName);
}
}
catch (DirectoryNotFoundException)
{
_logger.LogInformation(
"Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
}
catch (FileNotFoundException)
{
_logger.LogInformation(
"File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
}
catch (IOException)
{
if (isRequiredForDelete)
@ -443,7 +459,7 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
private static IEnumerable<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
{
var list = new List<string>
{
@ -461,7 +477,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="args">The args.</param>
/// <param name="resolvers">The resolvers.</param>
/// <returns>BaseItem.</returns>
private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers)
private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
{
var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
.FirstOrDefault(i => i is not null);
@ -474,7 +490,7 @@ namespace Emby.Server.Implementations.Library
return item;
}
private BaseItem Resolve(ItemResolveArgs args, IItemResolver resolver)
private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
{
try
{
@ -516,16 +532,16 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5();
}
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null)
public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
private BaseItem ResolvePath(
private BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
IDirectoryService directoryService,
IItemResolver[] resolvers,
Folder parent = null,
IItemResolver[]? resolvers,
Folder? parent = null,
CollectionType? collectionType = null,
LibraryOptions libraryOptions = null)
LibraryOptions? libraryOptions = null)
{
ArgumentNullException.ThrowIfNull(fileInfo);
@ -598,7 +614,7 @@ namespace Emby.Server.Implementations.Library
return ResolveItem(args, resolvers);
}
public bool IgnoreFile(FileSystemMetadata file, BaseItem parent)
public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
=> EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
@ -673,16 +689,16 @@ namespace Emby.Server.Implementations.Library
private IEnumerable<BaseItem> ResolveFileList(
IReadOnlyList<FileSystemMetadata> fileList,
IDirectoryService directoryService,
Folder parent,
Folder? parent,
CollectionType? collectionType,
IItemResolver[] resolvers,
IItemResolver[]? resolvers,
LibraryOptions libraryOptions)
{
// Given that fileList is a list we can save enumerator allocations by indexing
for (var i = 0; i < fileList.Count; i++)
{
var file = fileList[i];
BaseItem result = null;
BaseItem? result = null;
try
{
result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
@ -711,7 +727,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(rootFolderPath);
var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
(ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong"))
.DeepCopy<Folder, AggregateFolder>();
// In case program data folder was moved
@ -777,7 +793,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(userRootPath);
var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
UserRootFolder tmpItem = null;
UserRootFolder? tmpItem = null;
try
{
tmpItem = GetItemById(newItemId) as UserRootFolder;
@ -790,7 +806,8 @@ namespace Emby.Server.Implementations.Library
if (tmpItem is null)
{
_logger.LogDebug("Creating new userRootFolder with DeepCopy");
tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new InvalidOperationException("Failed to get user root path"))
.DeepCopy<Folder, UserRootFolder>();
}
// In case program data folder was moved
@ -809,7 +826,8 @@ namespace Emby.Server.Implementations.Library
return _userRootFolder;
}
public BaseItem FindByPath(string path, bool? isFolder)
/// <inheritdoc />
public BaseItem? FindByPath(string path, bool? isFolder)
{
// If this returns multiple items it could be tricky figuring out which one is correct.
// In most cases, the newest one will be and the others obsolete but not yet cleaned up
@ -828,12 +846,8 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault();
}
/// <summary>
/// Gets the person.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>Task{Person}.</returns>
public Person GetPerson(string name)
/// <inheritdoc />
public Person? GetPerson(string name)
{
var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path);
@ -1015,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
}
}
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken)
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
@ -1024,7 +1038,8 @@ namespace Emby.Server.Implementations.Library
new Progress<double>(),
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false,
cancellationToken).ConfigureAwait(false);
allowRemoveRoot: removeRoot,
cancellationToken: cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
@ -1032,7 +1047,8 @@ namespace Emby.Server.Implementations.Library
new Progress<double>(),
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false,
cancellationToken).ConfigureAwait(false);
allowRemoveRoot: removeRoot,
cancellationToken: cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
@ -1050,7 +1066,7 @@ namespace Emby.Server.Implementations.Library
var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
// Validate the entire media library
await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken: cancellationToken).ConfigureAwait(false);
progress.Report(96);
@ -1140,7 +1156,7 @@ namespace Emby.Server.Implementations.Library
.ToList();
}
private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid> refreshQueue)
private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? refreshQueue)
{
var info = new VirtualFolderInfo
{
@ -1204,20 +1220,15 @@ namespace Emby.Server.Implementations.Library
return null;
}
/// <summary>
/// Gets the item by id.
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
public BaseItem GetItemById(Guid id)
/// <inheritdoc />
public BaseItem? GetItemById(Guid id)
{
if (id.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
if (_cache.TryGetValue(id, out BaseItem item))
if (_cache.TryGetValue(id, out BaseItem? item))
{
return item;
}
@ -1233,7 +1244,7 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public T GetItemById<T>(Guid id)
public T? GetItemById<T>(Guid id)
where T : BaseItem
{
var item = GetItemById(id);
@ -1245,6 +1256,22 @@ namespace Emby.Server.Implementations.Library
return null;
}
/// <inheritdoc />
public T? GetItemById<T>(Guid id, Guid userId)
where T : BaseItem
{
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
return GetItemById<T>(id, user);
}
/// <inheritdoc />
public T? GetItemById<T>(Guid id, User? user)
where T : BaseItem
{
var item = GetItemById<T>(id);
return ItemIsVisible(item, user) ? item : null;
}
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
@ -1405,7 +1432,7 @@ namespace Emby.Server.Implementations.Library
var parents = new BaseItem[len];
for (int i = 0; i < len; i++)
{
parents[i] = GetItemById(ancestorIds[i]);
parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id: {ancestorIds[i]}");
if (parents[i] is not (ICollectionFolder or UserView))
{
return;
@ -1419,7 +1446,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
query.TopParentIds = new[] { Guid.NewGuid() };
query.TopParentIds = [Guid.NewGuid()];
}
}
@ -1516,7 +1543,7 @@ namespace Emby.Server.Implementations.Library
}
}
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User user)
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
{
if (item is UserView view)
{
@ -1585,16 +1612,20 @@ namespace Emby.Server.Implementations.Library
/// <returns>IEnumerable{System.String}.</returns>
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{
if (IntroProviders.Length == 0)
{
return [];
}
var tasks = IntroProviders
.Take(1)
.Select(i => GetIntros(i, item, user));
var items = await Task.WhenAll(tasks).ConfigureAwait(false);
return items
.SelectMany(i => i.ToArray())
.SelectMany(i => i)
.Select(ResolveIntro)
.Where(i => i is not null);
.Where(i => i is not null)!; // null values got filtered out
}
/// <summary>
@ -1623,9 +1654,9 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Video.</returns>
private Video ResolveIntro(IntroInfo info)
private Video? ResolveIntro(IntroInfo info)
{
Video video = null;
Video? video = null;
if (info.ItemId.HasValue)
{
@ -1676,29 +1707,26 @@ namespace Emby.Server.Implementations.Library
return video;
}
/// <summary>
/// Sorts the specified sort by.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="user">The user.</param>
/// <param name="sortBy">The sort by.</param>
/// <param name="sortOrder">The sort order.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
/// <inheritdoc />
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{
var isFirst = true;
IOrderedEnumerable<BaseItem> orderedItems = null;
IOrderedEnumerable<BaseItem>? orderedItems = null;
foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
{
if (isFirst)
{
orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy);
orderedItems = sortOrder == SortOrder.Descending
? items.OrderByDescending(i => i, orderBy)
: items.OrderBy(i => i, orderBy);
}
else
{
orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy);
orderedItems = sortOrder == SortOrder.Descending
? orderedItems!.ThenByDescending(i => i, orderBy)
: orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
}
isFirst = false;
@ -1707,11 +1735,12 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
/// <inheritdoc />
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
{
var isFirst = true;
IOrderedEnumerable<BaseItem> orderedItems = null;
IOrderedEnumerable<BaseItem>? orderedItems = null;
foreach (var (name, sortOrder) in orderBy)
{
@ -1723,11 +1752,15 @@ namespace Emby.Server.Implementations.Library
if (isFirst)
{
orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer);
orderedItems = sortOrder == SortOrder.Descending
? items.OrderByDescending(i => i, comparer)
: items.OrderBy(i => i, comparer);
}
else
{
orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, comparer) : orderedItems.ThenBy(i => i, comparer);
orderedItems = sortOrder == SortOrder.Descending
? orderedItems!.ThenByDescending(i => i, comparer)
: orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
}
isFirst = false;
@ -1742,14 +1775,14 @@ namespace Emby.Server.Implementations.Library
/// <param name="name">The name.</param>
/// <param name="user">The user.</param>
/// <returns>IBaseItemComparer.</returns>
private IBaseItemComparer GetComparer(ItemSortBy name, User user)
private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
{
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
{
var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType());
var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable<T> instances
userComparer.User = user;
userComparer.UserManager = _userManager;
@ -1761,23 +1794,14 @@ namespace Emby.Server.Implementations.Library
return comparer;
}
/// <summary>
/// Creates the item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="parent">The parent item.</param>
public void CreateItem(BaseItem item, BaseItem parent)
/// <inheritdoc />
public void CreateItem(BaseItem item, BaseItem? parent)
{
CreateItems(new[] { item }, parent, CancellationToken.None);
}
/// <summary>
/// Creates the items.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="parent">The parent item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
/// <inheritdoc />
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
{
_itemRepository.SaveItems(items, cancellationToken);
@ -2059,16 +2083,16 @@ namespace Emby.Server.Implementations.Library
public LibraryOptions GetLibraryOptions(BaseItem item)
{
if (item is not CollectionFolder collectionFolder)
if (item is CollectionFolder collectionFolder)
{
// List.Find is more performant than FirstOrDefault due to enumerator allocation
collectionFolder = GetCollectionFolders(item)
.Find(folder => folder is CollectionFolder) as CollectionFolder;
return collectionFolder.GetLibraryOptions();
}
return collectionFolder is null
? new LibraryOptions()
: collectionFolder.GetLibraryOptions();
// List.Find is more performant than FirstOrDefault due to enumerator allocation
return GetCollectionFolders(item)
.Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
? collectionFolder2.GetLibraryOptions()
: new LibraryOptions();
}
public CollectionType? GetContentType(BaseItem item)
@ -2422,7 +2446,7 @@ namespace Emby.Server.Implementations.Library
{
if (parentId.HasValue)
{
return GetItemById(parentId.Value);
return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}");
}
if (!userId.IsNullOrEmpty())
@ -2459,7 +2483,7 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo?
EpisodeInfo episodeInfo = null;
EpisodeInfo? episodeInfo = null;
if (episode.IsFileProtocol)
{
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
@ -2662,7 +2686,7 @@ namespace Emby.Server.Implementations.Library
}
}
BaseItem GetExtra(FileSystemMetadata file, ExtraType extraType)
BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType)
{
var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType));
if (extra is not Video && extra is not Audio)
@ -2677,16 +2701,21 @@ namespace Emby.Server.Implementations.Library
extra = itemById;
}
extra.ExtraType = extraType;
// Only update extra type if it is more specific then the currently known extra type
if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
{
extra.ExtraType = extraType;
}
extra.ParentId = Guid.Empty;
extra.OwnerId = owner.Id;
return extra;
}
}
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
{
string newPath;
string? newPath;
if (ownerItem is not null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@ -2760,8 +2789,8 @@ namespace Emby.Server.Implementations.Library
}
})
.Where(i => i is not null)
.Where(i => query.User is null || i.IsVisible(query.User))
.ToList();
.Where(i => query.User is null || i!.IsVisible(query.User))
.ToList()!; // null values are filtered out
}
public List<string> GetPeopleNames(InternalPeopleQuery query)
@ -2863,7 +2892,7 @@ namespace Emby.Server.Implementations.Library
if (collectionType is not null)
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
}
@ -2897,7 +2926,7 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
List<BaseItem> personsToSave = null;
List<BaseItem>? personsToSave = null;
foreach (var person in people)
{
@ -3010,9 +3039,7 @@ namespace Emby.Server.Implementations.Library
{
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
var list = libraryOptions.PathInfos.ToList();
list.Add(pathInfo);
libraryOptions.PathInfos = list.ToArray();
libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
@ -3031,8 +3058,7 @@ namespace Emby.Server.Implementations.Library
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
var list = libraryOptions.PathInfos.ToList();
foreach (var originalPathInfo in list)
foreach (var originalPathInfo in libraryOptions.PathInfos)
{
if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
{
@ -3041,8 +3067,6 @@ namespace Emby.Server.Implementations.Library
}
}
libraryOptions.PathInfos = list.ToArray();
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
}
@ -3095,7 +3119,7 @@ namespace Emby.Server.Implementations.Library
if (refreshLibrary)
{
await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
StartScanInBackground();
}
@ -3115,7 +3139,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(path));
}
List<NameValuePair> removeList = null;
List<NameValuePair>? removeList = null;
foreach (var contentType in _configurationManager.Configuration.ContentTypes)
{
@ -3168,5 +3192,20 @@ namespace Emby.Server.Implementations.Library
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
}
private static bool ItemIsVisible(BaseItem? item, User? user)
{
if (item is null)
{
return false;
}
if (user is null)
{
return true;
}
return item is UserRootFolder || item.IsVisibleStandalone(user);
}
}
}

@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
if (user is not null)
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
if (item.MediaType == MediaType.Audio)
{
@ -274,7 +274,7 @@ namespace Emby.Server.Implementations.Library
var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
return results.SelectMany(i => i.ToList());
return results.SelectMany(i => i);
}
private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken)
@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Library
catch (Exception ex)
{
_logger.LogError(ex, "Error getting media sources");
return Enumerable.Empty<MediaSourceInfo>();
return [];
}
}
@ -339,7 +339,7 @@ namespace Emby.Server.Implementations.Library
{
foreach (var source in sources)
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
if (item.MediaType == MediaType.Audio)
{
@ -360,7 +360,7 @@ namespace Emby.Server.Implementations.Library
{
if (string.IsNullOrEmpty(language))
{
return Array.Empty<string>();
return [];
}
var culture = _localizationManager.FindLanguageInfo(language);
@ -369,14 +369,15 @@ namespace Emby.Server.Implementations.Library
return culture.ThreeLetterISOLanguageNames;
}
return new string[] { language };
return [language];
}
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData.SubtitleStreamIndex.HasValue
&& user.RememberSubtitleSelections
&& user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
&& user.SubtitleMode != SubtitlePlaybackMode.None
&& allowRememberingSelection)
{
var index = userData.SubtitleStreamIndex.Value;
// Make sure the saved index is still valid
@ -390,7 +391,7 @@ namespace Emby.Server.Implementations.Library
var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex is null
var audioLanguage = defaultAudioIndex is null
? null
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
@ -398,9 +399,9 @@ namespace Emby.Server.Implementations.Library
source.MediaStreams,
preferredSubs,
user.SubtitleMode,
audioLangage);
audioLanguage);
MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage);
}
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
@ -421,7 +422,7 @@ namespace Emby.Server.Implementations.Library
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
{
// Item would only be null if the app didn't supply ItemId as part of the live stream open request
var mediaType = item?.MediaType ?? MediaType.Video;
@ -526,7 +527,7 @@ namespace Emby.Server.Implementations.Library
var item = request.ItemId.IsEmpty()
? null
: _libraryManager.GetItemById(request.ItemId);
SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
SetDefaultAudioAndSubtitleStreamIndices(item, clone, user);
}
return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);

@ -124,16 +124,16 @@ namespace Emby.Server.Implementations.Library
}
else if (mode == SubtitlePlaybackMode.Always)
{
// always load the most suitable full subtitles
// Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
// always load the most suitable full subtitles
// Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
}
// load forced subs if we have found no suitable full subtitles
// Load forced subs if we have found no suitable full subtitles
var iterStreams = filteredStreams is null || filteredStreams.Count == 0
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
: filteredStreams;

@ -31,8 +31,9 @@ namespace Emby.Server.Implementations.Library
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
// Must be at least 3 characters after the attribute =, ], any character.
var maxIndex = str.Length - attribute.Length - 3;
// Must be at least 3 characters after the attribute =, ], any character,
// then we offset it by 1, because we want the index and not length.
var maxIndex = str.Length - attribute.Length - 2;
while (attributeIndex > -1 && attributeIndex < maxIndex)
{
var attributeEnd = attributeIndex + attribute.Length;

@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.Library
item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) ||
item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values

@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;

@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml"))
if (filename.Contains("[boxset]", StringComparison.OrdinalIgnoreCase) || args.ContainsFileSystemEntryByName("collection.xml"))
{
return new BoxSet
{

@ -1,7 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -11,7 +9,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
{
@ -20,11 +17,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
public class PlaylistResolver : GenericFolderResolver<Playlist>
{
private CollectionType?[] _musicPlaylistCollectionTypes =
{
private readonly CollectionType?[] _musicPlaylistCollectionTypes =
[
null,
CollectionType.music
};
];
/// <inheritdoc/>
protected override Playlist Resolve(ItemResolveArgs args)

@ -303,8 +303,8 @@ namespace Emby.Server.Implementations.Library
{
// Handle situations with the grouping setting, e.g. movies showing up in tv, etc.
// Thanks to mixed content libraries included in the UserView
var hasCollectionType = parents.OfType<UserView>().ToArray();
if (hasCollectionType.Length > 0)
var hasCollectionType = parents.OfType<UserView>().ToList();
if (hasCollectionType.Count > 0)
{
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{

@ -64,6 +64,11 @@ namespace Emby.Server.Implementations.Library.Validators
try
{
var item = _libraryManager.GetPerson(person);
if (item is null)
{
_logger.LogWarning("Failed to get person: {Name}", person);
continue;
}
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@ -92,7 +97,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Person },
IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});

@ -126,5 +126,9 @@
"External": "خارجي",
"HearingImpaired": "ضعاف السمع",
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة."
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
"TaskAudioNormalization": "تطبيع الصوت",
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت."
}

@ -125,5 +125,7 @@
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках."
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць."
}

@ -126,5 +126,9 @@
"External": "Extern",
"HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades."
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
"TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció",
"TaskAudioNormalization": "Normalització d'Àudio",
"TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio."
}

@ -126,5 +126,9 @@
"External": "Externí",
"HearingImpaired": "Sluchově postižení",
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
"TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
"TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
"TaskAudioNormalization": "Normalizace zvuku",
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku."
}

@ -126,5 +126,7 @@
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet",
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker."
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere."
}

@ -126,5 +126,9 @@
"External": "Extern",
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
"TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
"TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken.",
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
"TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
"TaskAudioNormalization": "Audio Normalisierung",
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten."
}

@ -126,5 +126,9 @@
"External": "Εξωτερικό",
"HearingImpaired": "Με προβλήματα ακοής",
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον."
}

@ -126,5 +126,9 @@
"External": "External",
"HearingImpaired": "Hearing Impaired",
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries."
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
"TaskAudioNormalization": "Audio Normalisation",
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data."
}

@ -13,7 +13,7 @@
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"External": "External",
"FailedLoginAttemptWithUserName": "Failed login try from {0}",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Forced": "Forced",
@ -106,6 +106,8 @@
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images",
"TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
"TaskAudioNormalization": "Audio Normalization",
"TaskAudioNormalizationDescription": "Scans files for audio normalization data.",
"TaskRefreshLibrary": "Scan Media Library",
"TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
"TaskCleanLogs": "Clean Log Directory",
@ -125,5 +127,7 @@
"TaskOptimizeDatabase": "Optimize database",
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
"TaskKeyframeExtractor": "Keyframe Extractor",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time."
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist."
}

@ -124,5 +124,11 @@
"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",
"HearingImpaired": "Discapacidad Auditiva"
"HearingImpaired": "Discapacidad Auditiva",
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción."
}

@ -30,7 +30,7 @@
"ItemAddedWithName": "{0} se ha añadido a la biblioteca",
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
"LabelRunningTimeValue": "Duración: {0}",
"Latest": "Últimas",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
@ -126,5 +126,9 @@
"External": "Externo",
"HearingImpaired": "Discapacidad Auditiva",
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas."
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.",
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización."
}

@ -125,5 +125,7 @@
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
"TaskAudioNormalization": "Heli Normaliseerimine",
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks."
}

@ -126,5 +126,7 @@
"External": "خارجی",
"HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه."
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
"TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
"TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند."
}

@ -125,5 +125,9 @@
"External": "Ulkoinen",
"HearingImpaired": "Kuulorajoitteinen",
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista."
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja."
}

@ -11,7 +11,7 @@
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
"FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
@ -126,5 +126,9 @@
"External": "Externe",
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
}

@ -1,6 +1,6 @@
{
"Albums": "Albums",
"AppDeviceValues": "Application: {0}, Appareil: {1}",
"AppDeviceValues": "Application : {0}, Appareil: {1}",
"Application": "Application",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
@ -29,7 +29,7 @@
"Inherit": "Hériter",
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
"LabelIpAddressValue": "Adresse IP: {0}",
"LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Derniers",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
@ -126,5 +126,9 @@
"External": "Externe",
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
}

@ -1,13 +1,13 @@
{
"Albums": "Albumok",
"AppDeviceValues": "Program: {0}, eszköz: {1}",
"AppDeviceValues": "Program: {0}, Eszköz: {1}",
"Application": "Alkalmazás",
"Artists": "Előadók",
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
"Books": "Könyvek",
"CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}",
"CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}",
"Channels": "Csatornák",
"ChapterNameValue": "{0}. jelenet",
"ChapterNameValue": "Jelenet {0}",
"Collections": "Gyűjtemények",
"DeviceOfflineWithName": "{0} kijelentkezett",
"DeviceOnlineWithName": "{0} belépett",
@ -15,27 +15,27 @@
"Favorites": "Kedvencek",
"Folders": "Könyvtárak",
"Genres": "Műfajok",
"HeaderAlbumArtists": "Albumelőadók",
"HeaderAlbumArtists": "Album előadók",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteAlbums": "Kedvenc Albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
"HeaderFavoriteEpisodes": "Kedvenc epizódok",
"HeaderFavoriteShows": "Kedvenc sorozatok",
"HeaderFavoriteSongs": "Kedvenc számok",
"HeaderFavoriteSongs": "Kedvenc dalok",
"HeaderLiveTV": "Élő TV",
"HeaderNextUp": "Következik",
"HeaderRecordingGroups": "Felvételi csoportok",
"HomeVideos": "Házi videók",
"HomeVideos": "Otthoni videók",
"Inherit": "Örökölt",
"ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
"ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
"LabelIpAddressValue": "IP-cím: {0}",
"LabelRunningTimeValue": "Lejátszási idő: {0}",
"Latest": "Legújabb",
"MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve",
"MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett",
"MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}",
"MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve",
"MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}",
"MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve lett",
"MixedContent": "Vegyes tartalom",
"Movies": "Filmek",
"Music": "Zenék",
@ -46,7 +46,7 @@
"NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.",
"NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
"NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
"NotificationOptionAudioPlayback": "Hanglejátszás elkezdve",
"NotificationOptionAudioPlayback": "Hanglejátszás elkezdődött",
"NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva",
"NotificationOptionCameraImageUploaded": "Kamerakép feltöltve",
"NotificationOptionInstallationFailed": "Telepítési hiba",
@ -126,5 +126,9 @@
"External": "Külső",
"HearingImpaired": "Hallássérült",
"TaskRefreshTrickplayImages": "Trickplay képek generálása",
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz."
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
"TaskAudioNormalization": "Hangerő Normalizáció",
"TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
"TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.",
"TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása"
}

@ -126,5 +126,9 @@
"External": "Esterno",
"HearingImpaired": "con problemi di udito",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate."
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
"TaskCleanCollectionsAndPlaylists": "Ripulire le raccolte e le playlist",
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle raccolte e dalle playlist che non esistono più.",
"TaskAudioNormalization": "Normalizzazione Audio",
"TaskAudioNormalizationDescription": "Scansione files per normalizzazione audio."
}

@ -125,5 +125,9 @@
"External": "外部",
"HearingImpaired": "聴覚障害の方",
"TaskRefreshTrickplayImages": "トリックプレー画像を生成",
"TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。"
"TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。",
"TaskCleanCollectionsAndPlaylists": "コレクションとプレイリストをクリーンアップ",
"TaskAudioNormalization": "音声の正規化",
"TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。",
"TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。"
}

@ -124,5 +124,6 @@
"TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
"TaskKeyframeExtractor": "키프레임 추출",
"External": "외부",
"HearingImpaired": "청각 장애"
"HearingImpaired": "청각 장애",
"TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리"
}

@ -126,5 +126,7 @@
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose."
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
"TaskCleanCollectionsAndPlaylists": "Sutvarko duomenis jūsų kolekcijose ir grojaraščiuose.",
"TaskCleanCollectionsAndPlaylistsDescription": "Pašalina nebeegzistuojančius elementus iš kolekcijų ir grojaraščių."
}

@ -123,5 +123,7 @@
"HearingImpaired": "കേൾവി തകരാറുകൾ",
"External": "പുറമേയുള്ള",
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ",
"TaskCleanCollectionsAndPlaylistsDescription": "നിലവിലില്ലാത്ത ശേഖരങ്ങളിൽ നിന്നും പ്ലേലിസ്റ്റുകളിൽ നിന്നും ഇനങ്ങൾ നീക്കംചെയ്യുന്നു.",
"TaskCleanCollectionsAndPlaylists": "ശേഖരങ്ങളും പ്ലേലിസ്റ്റുകളും വൃത്തിയാക്കുക"
}

@ -0,0 +1,133 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparat: {1}",
"Application": "Applikazzjoni",
"Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{1} awtentikat b'suċċess",
"Books": "Kotba",
"CameraImageUploadedFrom": "Ttellgħet immaġni ġdida tal-kamera minn {1}",
"Channels": "Kanali",
"ChapterNameValue": "Kapitlu {0}",
"Collections": "Kollezzjonijiet",
"DeviceOfflineWithName": "{0} inqatgħa",
"DeviceOnlineWithName": "{0} qabad",
"External": "Estern",
"FailedLoginAttemptWithUserName": "Tentattiv t'aċċess fallut minn {0}",
"Favorites": "Favoriti",
"Forced": "Sfurzat",
"Genres": "Ġeneri",
"HeaderAlbumArtists": "Artisti tal-album",
"HeaderContinueWatching": "Kompli Segwi",
"HeaderFavoriteAlbums": "Albums Favoriti",
"HeaderFavoriteArtists": "Artisti Favoriti",
"HeaderFavoriteEpisodes": "Episodji Favoriti",
"HeaderFavoriteShows": "Programmi Favoriti",
"HeaderFavoriteSongs": "Kanzunetti Favoriti",
"HeaderNextUp": "Li Jmiss",
"SubtitleDownloadFailureFromForItem": "Is-sottotitli naqsu milli jitniżżlu minn {0} għal {1}",
"UserPasswordChangedWithName": "Il-password inbidel għall-utent {0}",
"TaskUpdatePluginsDescription": "Iniżżel u jinstalla aġġornamenti għal plugins li huma kkonfigurati biex jaġġornaw awtomatikament.",
"TaskDownloadMissingSubtitlesDescription": "Ifittex fuq l-internet għal sottotitli neqsin abbażi tal-konfigurazzjoni tal-metadata.",
"TaskOptimizeDatabaseDescription": "Jikkompatti d-database u jaqta' l-ispazju ħieles. It-tħaddim ta' dan il-kompitu wara li tiskennja l-librerija jew tagħmel bidliet oħra li jimplikaw modifiki fid-database jistgħu jtejbu l-prestazzjoni.",
"Default": "Standard",
"Folders": "Folders",
"HeaderLiveTV": "TV Dirett",
"HeaderRecordingGroups": "Gruppi ta' Reġistrazzjoni",
"HearingImpaired": "Nuqqas ta' Smigħ",
"HomeVideos": "Vidjows Personali",
"Inherit": "Jiret",
"ItemAddedWithName": "{0} ġie miżjud mal-librerija",
"ItemRemovedWithName": "{0} tneħħa mil-librerija",
"LabelIpAddressValue": "Indirizz IP: {0}",
"Latest": "Tal-Aħħar",
"MessageApplicationUpdated": "Jellyfin Server ġie aġġornat",
"MessageApplicationUpdatedTo": "JellyFin Server ġie aġġornat għal {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Is-sezzjoni {0} tal-konfigurazzjoni tas-server ġiet aġġornata",
"MessageServerConfigurationUpdated": "Il-konfigurazzjoni tas-server ġiet aġġornata",
"MixedContent": "Kontenut imħallat",
"Movies": "Films",
"Music": "Mużika",
"MusicVideos": "Vidjows tal-Mużika",
"NameInstallFailed": "L-installazzjoni ta' {0} falliet",
"NameSeasonNumber": "Staġun {0}",
"NameSeasonUnknown": "Staġun Mhux Magħruf",
"NewVersionIsAvailable": "Verżjoni ġdida ta' Jellyfin Server hija disponibbli biex titniżżel.",
"NotificationOptionApplicationUpdateAvailable": "Aġġornament tal-applikazzjoni disponibbli",
"NotificationOptionCameraImageUploaded": "Immaġini tal-kamera mtella'",
"LabelRunningTimeValue": "Tul: {0}",
"NotificationOptionApplicationUpdateInstalled": "Aġġornament tal-applikazzjoni ġie installat",
"NotificationOptionAudioPlayback": "Il-playback tal-awdjo beda",
"NotificationOptionAudioPlaybackStopped": "Il-playback tal-awdjo twaqqaf",
"NotificationOptionInstallationFailed": "Installazzjoni falliet",
"NotificationOptionNewLibraryContent": "Kontenut ġdid miżjud",
"NotificationOptionPluginError": "Ħsara fil-plugin",
"NotificationOptionPluginInstalled": "Plugin installat",
"NotificationOptionPluginUninstalled": "Plugin tneħħa",
"NotificationOptionServerRestartRequired": "Meħtieġ l-istartjar mill-ġdid tas-server",
"NotificationOptionTaskFailed": "Falliment tal-kompitu skedat",
"NotificationOptionUserLockedOut": "Utent imsakkar",
"Photos": "Ritratti",
"Playlists": "Playlists",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} ġie installat",
"PluginUninstalledWithName": "{0} ġie mneħħi",
"PluginUpdatedWithName": "{0} ġie aġġornat",
"ProviderValue": "Fornitur: {0}",
"ScheduledTaskFailedWithName": "{0} falla",
"ScheduledTaskStartedWithName": "{0} beda",
"ServerNameNeedsToBeRestarted": "{0} jeħtieġ li jerġa' jinbeda",
"Songs": "Kanzunetti",
"StartupEmbyServerIsLoading": "Jellyfin Server qed jixgħel. Jekk jogħġbok erġa' pprova dalwaqt.",
"Sync": "Sinkronizza",
"System": "Sistema",
"Undefined": "Mhux Definit",
"User": "Utent",
"UserCreatedWithName": "L-utent {0} inħoloq",
"UserDeletedWithName": "L-utent {0} tħassar",
"UserDownloadingItemWithValues": "{0} qed iniżżel {1}",
"UserLockedOutWithName": "L-utent {0} ġie msakkar",
"UserOfflineFromDevice": "{0} skonnettja minn {1}",
"UserOnlineFromDevice": "{0} huwa online minn {1}",
"NotificationOptionPluginUpdateInstalled": "Aġġornament ta' plugin ġie installat",
"NotificationOptionVideoPlayback": "Il-playback tal-vidjow beda",
"NotificationOptionVideoPlaybackStopped": "Il-playback tal-vidjow waqaf",
"Shows": "Programmi",
"TvShows": "Programmi tat-TV",
"UserPolicyUpdatedWithName": "Il-policy tal-utent ġiet aġġornata għal {0}",
"UserStartedPlayingItemWithValues": "{0} qed iħaddem {1} fuq {2}",
"UserStoppedPlayingItemWithValues": "{0} waqaf iħaddem {1} fuq {2}",
"ValueHasBeenAddedToLibrary": "{0} ġie miżjud mal-librerija tal-midja tiegħek",
"ValueSpecialEpisodeName": "Speċjali - {0}",
"VersionNumber": "Verżjoni {0}",
"TasksMaintenanceCategory": "Manutenzjoni",
"TasksLibraryCategory": "Librerija",
"TasksApplicationCategory": "Applikazzjoni",
"TasksChannelsCategory": "Kanali tal-Internet",
"TaskCleanActivityLog": "Naddaf il-Logg tal-Attività",
"TaskCleanActivityLogDescription": "Iħassar l-entrati tar-reġistru tal-attività eqdem mill-età kkonfigurata.",
"TaskCleanCache": "Naddaf id-Direttorju tal-Cache",
"TaskCleanCacheDescription": "Iħassar il-fajls tal-cache li m'għadhomx meħtieġa mis-sistema.",
"TaskRefreshChapterImages": "Oħroġ l-Immaġini tal-Kapitolu",
"TaskRefreshChapterImagesDescription": "Joħloq thumbnails għal vidjows li għandhom kapitli.",
"TaskAudioNormalization": "Normalizzazzjoni Awdjo",
"TaskAudioNormalizationDescription": "Skennja fajls għal data ta' normalizzazzjoni awdjo.",
"TaskRefreshLibrary": "Skennja l-Librerija tal-Midja",
"TaskRefreshLibraryDescription": "Jiskennja l-librerija tal-midja tiegħek għal fajls ġodda u jġedded il-metadejta.",
"TaskCleanLogs": "Naddaf id-Direttorju tal-Logg",
"TaskCleanLogsDescription": "Iħassar fajls tal-logg eqdem minn {0} ijiem.",
"TaskRefreshPeople": "Aġġorna Persuni",
"TaskRefreshPeopleDescription": "Jaġġorna l-metadejta għall-atturi u d-diretturi fil-librerija tal-midja tiegħek.",
"TaskRefreshTrickplayImages": "Iġġenera Stampi Trickplay",
"TaskRefreshTrickplayImagesDescription": "Joħloq previews trickplay għal vidjows fil-libreriji attivati.",
"TaskUpdatePlugins": "Aġġorna il-Plugins",
"TaskCleanTranscode": "Naddaf id-Direttorju tat-Transcode",
"TaskCleanTranscodeDescription": "Iħassar fajls transcode eqdem minn ġurnata.",
"TaskRefreshChannels": "Aġġorna l-Kanali",
"TaskRefreshChannelsDescription": "Aġġorna l-informazzjoni tal-kanali tal-internet.",
"TaskDownloadMissingSubtitles": "Niżżel is-sottotitli nieqsa",
"TaskOptimizeDatabase": "Ottimizza d-database",
"TaskKeyframeExtractor": "Estrattur ta' Keyframes",
"TaskKeyframeExtractorDescription": "Jiġbed il-keyframes mill-fajls tal-vidjow biex joħloq playlists HLS aktar preċiżi. Dan il-kompitu jista' jdum għal żmien twil.",
"TaskCleanCollectionsAndPlaylists": "Naddaf il-kollezzjonijiet u l-playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Ineħħi oġġetti minn kollezzjonijiet u playlists li m'għadhomx jeżistu."
}

@ -48,7 +48,7 @@
"Undefined": "သတ်မှတ်မထားသော",
"TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
"System": "စနစ်",
"Sync": "ထပ်တူကျသည်။",
"Sync": "ချိန်ကိုက်မည်",
"SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
"StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
"Songs": "သီချင်းများ",
@ -104,7 +104,7 @@
"HeaderFavoriteSongs": "အကြိုက်ဆုံးသီချင်းများ",
"HeaderFavoriteShows": "အကြိုက်ဆုံး ဇာတ်လမ်းတွဲများ",
"HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
"HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ",
"HeaderFavoriteArtists": "အကြိုက်ဆုံး အနုပညာရှင်များ",
"HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
"HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
"HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
@ -120,5 +120,11 @@
"AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
"Application": "အပလီကေးရှင်း",
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
"External": "ပြင်ပ"
"External": "ပြင်ပ",
"TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။",
"TaskCleanCollectionsAndPlaylistsDescription": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများမှ မရှိတော့သည်များကို ဖယ်ရှားမည်။",
"TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်",
"TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း",
"TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်",
"HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ"
}

@ -11,7 +11,7 @@
"Collections": "Collecties",
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
"DeviceOnlineWithName": "{0} is verbonden",
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
@ -126,5 +126,9 @@
"External": "Extern",
"HearingImpaired": "Slechthorend",
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld."
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
"TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten.",
"TaskAudioNormalization": "Geluidsnormalisatie",
"TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie."
}

@ -11,7 +11,7 @@
"Collections": "Kolekcje",
"DeviceOfflineWithName": "{0} został rozłączony",
"DeviceOnlineWithName": "{0} połączył się",
"FailedLoginAttemptWithUserName": "Próba logowania przez {0} zakończona niepowodzeniem",
"FailedLoginAttemptWithUserName": "Nieudana próba logowania przez {0}",
"Favorites": "Ulubione",
"Folders": "Foldery",
"Genres": "Gatunki",
@ -126,5 +126,9 @@
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
"HearingImpaired": "Niedosłyszący",
"TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach."
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.",
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
"TaskAudioNormalization": "Normalizacja dźwięku",
"TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku."
}

@ -111,7 +111,7 @@
"TaskCleanCacheDescription": "Deletar arquivos temporários que não são mais necessários para o sistema.",
"TaskCleanCache": "Limpar Arquivos Temporários",
"TasksChannelsCategory": "Canais da Internet",
"TasksApplicationCategory": "Aplicativo",
"TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskCleanActivityLogDescription": "Apaga o registro de atividades mais antigo que a idade configurada.",
@ -126,5 +126,9 @@
"External": "Externo",
"HearingImpaired": "Deficiência Auditiva",
"TaskRefreshTrickplayImages": "Gerar imagens Trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado."
"TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado.",
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio."
}

@ -126,5 +126,9 @@
"External": "Externo",
"HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar imagens de truques",
"TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas."
"TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio"
}

@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
"TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas."
"TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio"
}

@ -126,5 +126,7 @@
"External": "Внешние",
"HearingImpaired": "Для слабослышащих",
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена."
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют."
}

@ -126,5 +126,9 @@
"External": "Externé",
"HearingImpaired": "Sluchovo postihnutí",
"TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
"TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach."
"TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.",
"TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty",
"TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.",
"TaskAudioNormalization": "Normalizácia zvuku",
"TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku."
}

@ -126,5 +126,6 @@
"External": "Extern",
"HearingImpaired": "Hörselskadad",
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek."
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
"TaskCleanCollectionsAndPlaylists": "Rensa samlingar och spellistor"
}

@ -125,5 +125,9 @@
"External": "வெளி",
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்.",
"TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
"TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
"TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
"TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது."
}

@ -126,5 +126,9 @@
"External": "Harici",
"HearingImpaired": "Duyma Engelli",
"TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur",
"TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur."
"TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur.",
"TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.",
"TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin",
"TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.",
"TaskAudioNormalization": "Ses Normalleştirme"
}

@ -83,7 +83,7 @@
"SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
"Songs": "Пісні",
"Shows": "Телепередачі",
"Shows": "Серіали",
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
"ScheduledTaskStartedWithName": "{0} розпочато",
"ScheduledTaskFailedWithName": "{0} незавершено, збій",
@ -125,5 +125,9 @@
"External": "Зовнішній",
"HearingImpaired": "З порушеннями слуху",
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
"TaskRefreshTrickplayImages": "Створення Trickplay-зображень"
"TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
"TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
"TaskAudioNormalization": "Нормалізація аудіо"
}

@ -125,5 +125,7 @@
"External": "Bên ngoài",
"HearingImpaired": "Khiếm Thính",
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật."
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại."
}

@ -126,5 +126,9 @@
"External": "外部",
"HearingImpaired": "听力障碍",
"TaskRefreshTrickplayImages": "生成时间轴缩略图",
"TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。"
"TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。",
"TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
"TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。",
"TaskAudioNormalization": "音频标准化",
"TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。"
}

@ -1,25 +1,25 @@
{
"Albums": "專輯",
"AppDeviceValues": "App{0},裝置:{1}",
"AppDeviceValues": "應用程式{0},裝置:{1}",
"Application": "應用程式",
"Artists": "演出者",
"AuthenticationSucceededWithUserName": "{0} 成功授權",
"Books": "書",
"CameraImageUploadedFrom": "{0} 已經成功上傳一張相片",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "成功授權 {0}",
"Books": "",
"CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷線",
"DeviceOnlineWithName": "{0} 已經連線",
"FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入",
"Collections": "系列",
"DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
"HeaderContinueWatching": "繼續觀",
"HeaderContinueWatching": "繼續觀",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛演出者",
"HeaderFavoriteEpisodes": "最愛集",
"HeaderFavoriteArtists": "最愛藝人",
"HeaderFavoriteEpisodes": "最愛集",
"HeaderFavoriteShows": "最愛節目",
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderLiveTV": "電視直播",
@ -30,8 +30,8 @@
"LabelIpAddressValue": "IP 位址:{0}",
"LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server 已經更新",
"MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
"MessageApplicationUpdated": "Jellyfin 伺服器已經更新",
"MessageApplicationUpdatedTo": "Jellyfin 伺服器已經更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MixedContent": "混合內容",
@ -41,7 +41,7 @@
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
"NewVersionIsAvailable": "新版本的 Jellyfin Server 已經可供下載。",
"NewVersionIsAvailable": "新版本的 Jellyfin 伺服器已經可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式更新已安裝",
"NotificationOptionAudioPlayback": "音訊播放已開始",
@ -49,52 +49,52 @@
"NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
"NotificationOptionPluginError": "附加元件安裝失敗",
"NotificationOptionPluginInstalled": "附加元件已安裝",
"NotificationOptionPluginUninstalled": "附加元件已移除",
"NotificationOptionPluginUpdateInstalled": "附加元件已更新",
"NotificationOptionPluginError": "擴充功能錯誤",
"NotificationOptionPluginInstalled": "擴充功能已安裝",
"NotificationOptionPluginUninstalled": "擴充功能已移除",
"NotificationOptionPluginUpdateInstalled": "擴充功能已更新",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程任務失敗",
"NotificationOptionTaskFailed": "擴充功能任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
"NotificationOptionVideoPlayback": "影片播放已開始",
"NotificationOptionVideoPlaybackStopped": "影片播放已停止",
"Photos": "相片",
"Playlists": "播放清單",
"Plugin": "附加元件",
"PluginInstalledWithName": "{0} 已安裝",
"PluginUninstalledWithName": "{0} 已移除",
"PluginUpdatedWithName": "{0} 已更新",
"ProviderValue": "提供商: {0}",
"ScheduledTaskFailedWithName": "排程任務 {0} 失敗",
"Plugin": "擴充功能",
"PluginInstalledWithName": "已安裝 {0}",
"PluginUninstalledWithName": "已移除 {0}",
"PluginUpdatedWithName": "已更新 {0}",
"ProviderValue": "提供者:{0}",
"ScheduledTaskFailedWithName": "排程任務 {0} 執行失敗",
"ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
"ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin Server 載入中,請稍後再試。",
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。",
"Sync": "同步",
"System": "系統",
"TvShows": "電視節目",
"User": "使用者",
"UserCreatedWithName": "使用者 {0} 已建立",
"UserDeletedWithName": "使用者 {0} 已移除",
"UserCreatedWithName": "已建立使用者 {0}",
"UserDeletedWithName": "已刪除使用者 {0}",
"UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
"UserLockedOutWithName": "使用者 {0} 已鎖定",
"UserLockedOutWithName": "使用者 {0} 已鎖定",
"UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
"UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserPolicyUpdatedWithName": "使用者協議已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0}正在 {2} 上播放 {1}",
"UserPolicyUpdatedWithName": "使用者權限已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫",
"ValueSpecialEpisodeName": "特輯 - {0}",
"VersionNumber": "版本 {0}",
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
"TaskDownloadMissingSubtitlesDescription": "透過媒體資訊從網路上搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新附加元件",
"TaskUpdatePlugins": "更新擴充功能",
"TaskRefreshPeople": "更新人物",
"TaskCleanLogsDescription": "刪除超過 {0} 天的日誌文件。",
"TaskCleanLogs": "清空日誌資料夾",
@ -105,9 +105,9 @@
"TaskCleanCache": "清除快取資料夾",
"TasksLibraryCategory": "媒體庫",
"TaskRefreshChannelsDescription": "重新整理網路頻道資料。",
"TaskCleanTranscodeDescription": "刪除超過一天的轉。",
"TaskCleanTranscode": "清除轉資料夾",
"TaskUpdatePluginsDescription": "為已設置為自動更新的附加元件下載並安裝更新。",
"TaskCleanTranscodeDescription": "刪除超過一天的轉檔。",
"TaskCleanTranscode": "清除轉資料夾",
"TaskUpdatePluginsDescription": "下載並更新已啟用自動更新的擴充功能。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的資訊。",
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
"TasksChannelsCategory": "網路頻道",
@ -125,5 +125,7 @@
"External": "外部",
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "生成快轉縮圖",
"TaskRefreshTrickplayImagesDescription": "為啟用此設定的媒體庫生成快轉縮圖。"
"TaskRefreshTrickplayImagesDescription": "為啟用快轉縮圖的媒體庫生成快轉縮圖。",
"TaskCleanCollectionsAndPlaylists": "清理系列和播放清單",
"TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。"
}

@ -278,6 +278,13 @@ namespace Emby.Server.Implementations.Localization
return null;
}
// Convert integers directly
// This may override some of the locale specific age ratings (but those always map to the same age)
if (int.TryParse(rating, out var ratingAge))
{
return ratingAge;
}
// Fairly common for some users to have "Rated R" in their rating field
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
@ -314,7 +321,11 @@ namespace Emby.Server.Implementations.Localization
// Try splitting by : to handle "Germany: FSK-18"
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
{
return GetRatingLevel(rating.AsSpan().RightPart(':').ToString());
var ratingLevelRightPart = rating.AsSpan().RightPart(':');
if (ratingLevelRightPart.Length != 0)
{
return GetRatingLevel(ratingLevelRightPart.ToString());
}
}
// Handle prefix country code to handle "DE-18"
@ -325,8 +336,12 @@ namespace Emby.Server.Implementations.Localization
// Extract culture from country prefix
var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
// Check rating system of culture
return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
var ratingLevelRightPart = ratingSpan.RightPart('-');
if (ratingLevelRightPart.Length != 0)
{
// Check rating system of culture
return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
}
}
return null;

@ -59,68 +59,74 @@ namespace Emby.Server.Implementations.Playlists
_appConfig = appConfig;
}
public Playlist GetPlaylistForUser(Guid playlistId, Guid userId)
{
return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault();
}
public IEnumerable<Playlist> GetPlaylists(Guid userId)
{
var user = _userManager.GetUserById(userId);
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>();
return _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Playlist],
Recursive = true,
DtoOptions = new DtoOptions(false)
})
.Cast<Playlist>()
.Where(p => p.IsVisible(user));
}
public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options)
public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest request)
{
var name = options.Name;
var name = request.Name;
var folderName = _fileSystem.GetValidFilename(name);
var parentFolder = GetPlaylistsFolder(options.UserId);
var parentFolder = GetPlaylistsFolder(request.UserId);
if (parentFolder is null)
{
throw new ArgumentException(nameof(parentFolder));
}
if (options.MediaType is null || options.MediaType == MediaType.Unknown)
if (request.MediaType is null || request.MediaType == MediaType.Unknown)
{
foreach (var itemId in options.ItemIdList)
foreach (var itemId in request.ItemIdList)
{
var item = _libraryManager.GetItemById(itemId);
if (item is null)
{
throw new ArgumentException("No item exists with the supplied Id");
}
var item = _libraryManager.GetItemById(itemId) ?? throw new ArgumentException("No item exists with the supplied Id");
if (item.MediaType != MediaType.Unknown)
{
options.MediaType = item.MediaType;
request.MediaType = item.MediaType;
}
else if (item is MusicArtist || item is MusicAlbum || item is MusicGenre)
{
options.MediaType = MediaType.Audio;
request.MediaType = MediaType.Audio;
}
else if (item is Genre)
{
options.MediaType = MediaType.Video;
request.MediaType = MediaType.Video;
}
else
{
if (item is Folder folder)
{
options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
request.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
.Select(i => i.MediaType)
.FirstOrDefault(i => i != MediaType.Unknown);
}
}
if (options.MediaType is not null && options.MediaType != MediaType.Unknown)
if (request.MediaType is not null && request.MediaType != MediaType.Unknown)
{
break;
}
}
}
if (options.MediaType is null || options.MediaType == MediaType.Unknown)
if (request.MediaType is null || request.MediaType == MediaType.Unknown)
{
options.MediaType = MediaType.Audio;
request.MediaType = MediaType.Audio;
}
var user = _userManager.GetUserById(options.UserId);
var user = _userManager.GetUserById(request.UserId);
var path = Path.Combine(parentFolder.Path, folderName);
path = GetTargetPath(path);
@ -133,19 +139,20 @@ namespace Emby.Server.Implementations.Playlists
{
Name = name,
Path = path,
OwnerUserId = options.UserId,
Shares = options.Shares ?? Array.Empty<Share>()
OwnerUserId = request.UserId,
Shares = request.Users ?? [],
OpenAccess = request.Public ?? false
};
playlist.SetMediaType(options.MediaType);
playlist.SetMediaType(request.MediaType);
parentFolder.AddChild(playlist);
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false);
if (options.ItemIdList.Count > 0)
if (request.ItemIdList.Count > 0)
{
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
await AddToPlaylistInternal(playlist.Id, request.ItemIdList, user, new DtoOptions(false)
{
EnableImages = true
}).ConfigureAwait(false);
@ -160,7 +167,14 @@ namespace Emby.Server.Implementations.Playlists
}
}
private string GetTargetPath(string path)
private List<Playlist> GetUserPlaylists(Guid userId)
{
var user = _userManager.GetUserById(userId);
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>().ToList();
}
private static string GetTargetPath(string path)
{
while (Directory.Exists(path))
{
@ -170,14 +184,14 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
{
var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null);
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
}
public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
@ -224,20 +238,10 @@ namespace Emby.Server.Implementations.Playlists
return;
}
// Create a new array with the updated playlist items
var newLinkedChildren = new LinkedChild[playlist.LinkedChildren.Length + childrenToAdd.Count];
playlist.LinkedChildren.CopyTo(newLinkedChildren, 0);
childrenToAdd.CopyTo(newLinkedChildren, playlist.LinkedChildren.Length);
// Update the playlist in the repository
playlist.LinkedChildren = newLinkedChildren;
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
// Update the playlist on disk
if (playlist.IsFile)
{
SavePlaylistFile(playlist);
}
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
// Refresh playlist metadata
_providerManager.QueueRefresh(
@ -249,7 +253,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
public async Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
@ -266,12 +270,7 @@ namespace Emby.Server.Implementations.Playlists
.Select(i => i.Item1)
.ToArray();
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
SavePlaylistFile(playlist);
}
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
_providerManager.QueueRefresh(
playlist.Id,
@ -313,14 +312,9 @@ namespace Emby.Server.Implementations.Playlists
newList.Insert(newIndex, item);
}
playlist.LinkedChildren = newList.ToArray();
playlist.LinkedChildren = [.. newList];
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
SavePlaylistFile(playlist);
}
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
/// <inheritdoc />
@ -430,8 +424,11 @@ namespace Emby.Server.Implementations.Playlists
}
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
var playlist = new M3uPlaylist();
playlist.IsExtended = true;
var playlist = new M3uPlaylist
{
IsExtended = true
};
foreach (var child in item.GetLinkedChildren())
{
var entry = new M3uPlaylistEntry()
@ -481,7 +478,7 @@ namespace Emby.Server.Implementations.Playlists
}
}
private string NormalizeItemPath(string playlistPath, string itemPath)
private static string NormalizeItemPath(string playlistPath, string itemPath)
{
return MakeRelativePath(Path.GetDirectoryName(playlistPath), itemPath);
}
@ -516,11 +513,13 @@ namespace Emby.Server.Implementations.Playlists
return relativePath;
}
/// <inheritdoc />
public Folder GetPlaylistsFolder()
{
return GetPlaylistsFolder(Guid.Empty);
}
/// <inheritdoc />
public Folder GetPlaylistsFolder(Guid userId)
{
const string TypeName = "PlaylistsFolder";
@ -532,21 +531,16 @@ namespace Emby.Server.Implementations.Playlists
/// <inheritdoc />
public async Task RemovePlaylistsAsync(Guid userId)
{
var playlists = GetPlaylists(userId);
var playlists = GetUserPlaylists(userId);
foreach (var playlist in playlists)
{
// Update owner if shared
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray();
if (rankedShares.Length > 0 && Guid.TryParse(rankedShares[0].UserId, out var guid))
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToList();
if (rankedShares.Count > 0)
{
playlist.OwnerUserId = guid;
playlist.OwnerUserId = rankedShares[0].UserId;
playlist.Shares = rankedShares.Skip(1).ToArray();
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
SavePlaylistFile(playlist);
}
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
else if (!playlist.OpenAccess)
{
@ -563,5 +557,76 @@ namespace Emby.Server.Implementations.Playlists
}
}
}
public async Task UpdatePlaylist(PlaylistUpdateRequest request)
{
var playlist = GetPlaylistForUser(request.Id, request.UserId);
if (request.Ids is not null)
{
playlist.LinkedChildren = [];
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
var user = _userManager.GetUserById(request.UserId);
await AddToPlaylistInternal(request.Id, request.Ids, user, new DtoOptions(false)
{
EnableImages = true
}).ConfigureAwait(false);
playlist = GetPlaylistForUser(request.Id, request.UserId);
}
if (request.Name is not null)
{
playlist.Name = request.Name;
}
if (request.Users is not null)
{
playlist.Shares = request.Users;
}
if (request.Public is not null)
{
playlist.OpenAccess = request.Public.Value;
}
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
public async Task AddUserToShares(PlaylistUserUpdateRequest request)
{
var userId = request.UserId;
var playlist = GetPlaylistForUser(request.Id, userId);
var shares = playlist.Shares.ToList();
var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(userId));
if (existingUserShare is not null)
{
shares.Remove(existingUserShare);
}
shares.Add(new PlaylistUserPermissions(userId, request.CanEdit ?? false));
playlist.Shares = shares;
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
public async Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share)
{
var playlist = GetPlaylistForUser(playlistId, userId);
var shares = playlist.Shares.ToList();
shares.Remove(share);
playlist.Shares = shares;
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
private async Task UpdatePlaylistInternal(Playlist playlist)
{
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
SavePlaylistFile(playlist);
}
}
}
}

@ -256,8 +256,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
get
{
var triggers = InternalTriggers;
return triggers.Select(i => i.Item1).ToArray();
return Array.ConvertAll(InternalTriggers, i => i.Item1);
}
set
@ -269,7 +268,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
SaveTriggers(triggerList);
InternalTriggers = triggerList.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i)));
}
}
@ -503,7 +502,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
{
// This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
var settings = LoadTriggerSettings().Where(i => i is not null).ToArray();
var settings = LoadTriggerSettings().Where(i => i is not null);
return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
}

@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
/// <summary>
/// The audio normalization task.
/// </summary>
public partial class AudioNormalizationTask : IScheduledTask
{
private readonly IItemRepository _itemRepository;
private readonly ILibraryManager _libraryManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IConfigurationManager _configurationManager;
private readonly ILocalizationManager _localization;
private readonly ILogger<AudioNormalizationTask> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
/// </summary>
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>
public AudioNormalizationTask(
IItemRepository itemRepository,
ILibraryManager libraryManager,
IMediaEncoder mediaEncoder,
IConfigurationManager configurationManager,
ILocalizationManager localizationManager,
ILogger<AudioNormalizationTask> logger)
{
_itemRepository = itemRepository;
_libraryManager = libraryManager;
_mediaEncoder = mediaEncoder;
_configurationManager = configurationManager;
_localization = localizationManager;
_logger = logger;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskAudioNormalization");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
/// <inheritdoc />
public string Key => "AudioNormalization";
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
private static partial Regex LUFSRegex();
/// <inheritdoc />
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
foreach (var library in _libraryManager.RootFolder.Children)
{
var libraryOptions = _libraryManager.GetLibraryOptions(library);
if (!libraryOptions.EnableLUFSScan)
{
continue;
}
// Album gain
var albums = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicAlbum],
Parent = library,
Recursive = true
});
foreach (var a in albums)
{
if (a.NormalizationGain.HasValue || a.LUFS.HasValue)
{
continue;
}
// Skip albums that don't have multiple tracks, album gain is useless here
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
if (albumTracks.Count <= 1)
{
continue;
}
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
a.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
cancellationToken).ConfigureAwait(false);
File.Delete(tempFile);
}
_itemRepository.SaveItems(albums, cancellationToken);
// Track gain
var tracks = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = [MediaType.Audio],
IncludeItemTypes = [BaseItemKind.Audio],
Parent = library,
Recursive = true
});
foreach (var t in tracks)
{
if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)
{
continue;
}
t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken);
}
_itemRepository.SaveItems(tracks, cancellationToken);
}
}
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return
[
new TaskTriggerInfo
{
Type = TaskTriggerInfo.TriggerInterval,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
];
}
private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
{
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
using (var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = _mediaEncoder.EncoderPath,
Arguments = args,
RedirectStandardOutput = false,
RedirectStandardError = true
},
})
{
try
{
_logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
process.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);
return null;
}
using var reader = process.StandardError;
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
MatchCollection split = LUFSRegex().Matches(output);
if (split.Count != 0)
{
return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
}
_logger.LogError("Failed to find LUFS value in output:\n{Output}", output);
return null;
}
}
}

@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;

@ -116,7 +116,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
foreach (var linkedChild in folder.LinkedChildren)
{
var path = linkedChild.Path;
if (!File.Exists(path))
if (!File.Exists(path) && !Directory.Exists(path))
{
_logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
(itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);

@ -15,10 +15,9 @@ namespace Emby.Server.Implementations.Serialization
{
// Need to cache these
// http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html
private static readonly ConcurrentDictionary<string, XmlSerializer> _serializers =
new ConcurrentDictionary<string, XmlSerializer>();
private readonly ConcurrentDictionary<string, XmlSerializer> _serializers = new();
private static XmlSerializer GetSerializer(Type type)
private XmlSerializer GetSerializer(Type type)
=> _serializers.GetOrAdd(
type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
static (_, t) => new XmlSerializer(t),

@ -159,10 +159,7 @@ namespace Emby.Server.Implementations.Session
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
ObjectDisposedException.ThrowIf(_disposed, this);
}
private void OnSessionStarted(SessionInfo info)
@ -403,7 +400,7 @@ namespace Emby.Server.Implementations.Session
{
session.NowPlayingQueue = nowPlayingQueue;
var itemIds = nowPlayingQueue.Select(queue => queue.Id).ToArray();
var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
_libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
new DtoOptions(true));
@ -1389,16 +1386,13 @@ namespace Emby.Server.Implementations.Session
if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
{
var user = _userManager.GetUserById(userId);
var list = session.AdditionalUsers.ToList();
list.Add(new SessionUserInfo
var newUser = new SessionUserInfo
{
UserId = userId,
UserName = user.Username
});
};
session.AdditionalUsers = list.ToArray();
session.AdditionalUsers = [..session.AdditionalUsers, newUser];
}
}

@ -33,11 +33,6 @@ namespace Emby.Server.Implementations.Session
/// </summary>
private const float ForceKeepAliveFactor = 0.75f;
/// <summary>
/// Lock used for accessing the KeepAlive cancellation token.
/// </summary>
private readonly object _keepAliveLock = new object();
/// <summary>
/// The WebSocket watchlist.
/// </summary>
@ -55,7 +50,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// The KeepAlive cancellation token.
/// </summary>
private CancellationTokenSource? _keepAliveCancellationToken;
private System.Timers.Timer _keepAlive;
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
@ -71,12 +66,34 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
_keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
{
AutoReset = true,
Enabled = false
};
_keepAlive.Elapsed += KeepAliveSockets;
}
/// <inheritdoc />
public void Dispose()
{
StopKeepAlive();
if (_keepAlive is not null)
{
_keepAlive.Stop();
_keepAlive.Elapsed -= KeepAliveSockets;
_keepAlive.Dispose();
_keepAlive = null!;
}
lock (_webSocketsLock)
{
foreach (var webSocket in _webSockets)
{
webSocket.Closed -= OnWebSocketClosed;
}
_webSockets.Clear();
}
}
/// <summary>
@ -164,7 +181,7 @@ namespace Emby.Server.Implementations.Session
webSocket.Closed += OnWebSocketClosed;
webSocket.LastKeepAliveDate = DateTime.UtcNow;
StartKeepAlive();
_keepAlive.Start();
}
// Notify WebSocket about timeout
@ -186,66 +203,26 @@ namespace Emby.Server.Implementations.Session
{
lock (_webSocketsLock)
{
if (!_webSockets.Remove(webSocket))
{
_logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
}
else
if (_webSockets.Remove(webSocket))
{
webSocket.Closed -= OnWebSocketClosed;
}
}
}
/// <summary>
/// Starts the KeepAlive watcher.
/// </summary>
private void StartKeepAlive()
{
lock (_keepAliveLock)
{
if (_keepAliveCancellationToken is null)
{
_keepAliveCancellationToken = new CancellationTokenSource();
// Start KeepAlive watcher
_ = RepeatAsyncCallbackEvery(
KeepAliveSockets,
TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
_keepAliveCancellationToken.Token);
}
}
}
/// <summary>
/// Stops the KeepAlive watcher.
/// </summary>
private void StopKeepAlive()
{
lock (_keepAliveLock)
{
if (_keepAliveCancellationToken is not null)
else
{
_keepAliveCancellationToken.Cancel();
_keepAliveCancellationToken.Dispose();
_keepAliveCancellationToken = null;
_logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
}
}
lock (_webSocketsLock)
{
foreach (var webSocket in _webSockets)
if (_webSockets.Count == 0)
{
webSocket.Closed -= OnWebSocketClosed;
_keepAlive.Stop();
}
_webSockets.Clear();
}
}
/// <summary>
/// Checks status of KeepAlive of WebSockets.
/// </summary>
private async Task KeepAliveSockets()
private async void KeepAliveSockets(object? o, EventArgs? e)
{
List<IWebSocketConnection> inactive;
List<IWebSocketConnection> lost;
@ -291,11 +268,6 @@ namespace Emby.Server.Implementations.Session
RemoveWebSocket(webSocket);
}
}
if (_webSockets.Count == 0)
{
StopKeepAlive();
}
}
}
@ -310,29 +282,5 @@ namespace Emby.Server.Implementations.Session
new ForceKeepAliveMessage(WebSocketLostTimeout),
CancellationToken.None);
}
/// <summary>
/// Runs a given async callback once every specified interval time, until cancelled.
/// </summary>
/// <param name="callback">The async callback.</param>
/// <param name="interval">The interval time.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task RepeatAsyncCallbackEvery(Func<Task> callback, TimeSpan interval, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await callback().ConfigureAwait(false);
try
{
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
return;
}
}
}
}
}

@ -1,5 +1,4 @@
using System;
using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -9,37 +8,35 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
/// Class AlbumArtistComparer.
/// Allows comparing artists of albums. Only the first artist of each album is considered.
/// </summary>
public class AlbumArtistComparer : IBaseItemComparer
{
/// <summary>
/// Gets the name.
/// Gets the item type this comparer compares.
/// </summary>
/// <value>The name.</value>
public ItemSortBy Type => ItemSortBy.AlbumArtist;
/// <summary>
/// Compares the specified x.
/// Compares the specified arguments on their primary artist.
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
/// <param name="x">First item to compare.</param>
/// <param name="y">Second item to compare.</param>
/// <returns>Zero if equal, else negative or positive number to indicate order.</returns>
public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
return string.Compare(GetFirstAlbumArtist(x), GetFirstAlbumArtist(y), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Gets the value.
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
private static string? GetValue(BaseItem? x)
private static string? GetFirstAlbumArtist(BaseItem? x)
{
var audio = x as IHasAlbumArtist;
if (x is IHasAlbumArtist audio
&& audio.AlbumArtists.Count != 0)
{
return audio.AlbumArtists[0];
}
return audio?.AlbumArtists.FirstOrDefault();
return null;
}
}
}

@ -1,10 +1,6 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
@ -15,19 +11,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
public class FirstTimeSetupHandler : AuthorizationHandler<FirstTimeSetupRequirement>
{
private readonly IConfigurationManager _configurationManager;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupHandler" /> class.
/// </summary>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
public FirstTimeSetupHandler(
IConfigurationManager configurationManager,
IUserManager userManager)
public FirstTimeSetupHandler(IConfigurationManager configurationManager)
{
_configurationManager = configurationManager;
_userManager = userManager;
}
/// <inheritdoc />
@ -36,37 +27,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
var contextUser = context.User;
if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator))
else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
{
context.Fail();
return Task.CompletedTask;
}
var userId = contextUser.GetUserId();
if (userId.IsEmpty())
{
context.Fail();
return Task.CompletedTask;
}
if (!requirement.ValidateParentalSchedule)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
var user = _userManager.GetUserById(userId);
if (user is null)
{
throw new ResourceNotFoundException();
}
if (user.IsParentalScheduleAllowed())
else
{
// Any user-specific checks are handled in the DefaultAuthorizationHandler.
context.Succeed(requirement);
}

@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
[FromQuery] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -132,8 +132,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -261,12 +261,12 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -296,8 +296,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

@ -125,12 +125,15 @@ public class ConfigurationController : BaseJellyfinApiController
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
/// <response code="204">Media encoder path updated.</response>
/// <returns>Status.</returns>
[Obsolete("This endpoint is obsolete.")]
[ApiExplorerSettings(IgnoreApi = true)]
[HttpPost("MediaEncoder/Path")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
{
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
// API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES
// _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
return NoContent();
}
}

@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
@ -45,9 +46,9 @@ public class DashboardController : BaseJellyfinApiController
/// <response code="404">Server still loading.</response>
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
[HttpGet("web/ConfigurationPages")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize]
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu)
{

@ -194,7 +194,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);

@ -163,18 +163,18 @@ public class DynamicHlsController : BaseJellyfinApiController
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
[FromRoute, Required] Guid itemId,
[FromQuery] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -204,8 +204,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -406,12 +406,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -443,8 +443,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -577,12 +577,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -613,8 +613,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -742,12 +742,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -779,8 +779,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -909,12 +909,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -945,8 +945,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -1085,12 +1085,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -1122,8 +1122,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -1265,12 +1265,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@ -1301,8 +1301,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@ -1481,7 +1481,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (currentTranscodingIndex.HasValue)
{
DeleteLastFile(playlistPath, segmentExtension, 0);
await DeleteLastFile(playlistPath, segmentExtension, 0).ConfigureAwait(false);
}
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
@ -1712,12 +1712,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var channels = state.OutputAudioChannels;
var useDownMixAlgorithm = state.AudioStream.Channels is 6 && _encodingOptions.DownMixStereoAlgorithm != DownMixStereoAlgorithms.None;
if (channels.HasValue
&& (channels.Value != 2
|| (state.AudioStream is not null
&& state.AudioStream.Channels.HasValue
&& state.AudioStream.Channels.Value > 5
&& _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
|| (state.AudioStream?.Channels != null && !useDownMixAlgorithm)))
{
args += " -ac " + channels.Value;
}
@ -2010,17 +2009,19 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
if (file is not null)
if (file is null)
{
DeleteFile(file.FullName, retryCount);
return Task.CompletedTask;
}
return DeleteFile(file.FullName, retryCount);
}
private void DeleteFile(string path, int retryCount)
private async Task DeleteFile(string path, int retryCount)
{
if (retryCount >= 5)
{
@ -2037,9 +2038,8 @@ public class DynamicHlsController : BaseJellyfinApiController
{
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
var task = Task.Delay(100);
task.Wait();
DeleteFile(path, retryCount + 1);
await Task.Delay(100).ConfigureAwait(false);
await DeleteFile(path, retryCount + 1).ConfigureAwait(false);
}
catch (Exception ex)
{

@ -162,7 +162,7 @@ public class FilterController : BaseJellyfinApiController
}
else if (parentId.HasValue)
{
parentItem = _libraryManager.GetItemById(parentId.Value);
parentItem = _libraryManager.GetItemById<BaseItem>(parentId.Value);
}
var filters = new QueryFilters();

@ -90,6 +90,7 @@ public class ImageController : BaseJellyfinApiController
/// <param name="userId">User Id.</param>
/// <response code="204">Image updated.</response>
/// <response code="403">User does not have permission to delete the image.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("UserImage")]
[Authorize]
@ -97,6 +98,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PostUserImage(
[FromQuery] Guid? userId)
{
@ -289,7 +291,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromQuery] int? imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -317,7 +319,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -346,7 +348,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -390,7 +392,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -433,7 +435,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] int imageIndex,
[FromQuery, Required] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -456,7 +458,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -559,7 +561,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromQuery] int? imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -637,7 +639,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -715,7 +717,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromRoute, Required] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();

@ -62,9 +62,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Songs/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@ -75,11 +77,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -99,9 +106,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Albums/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@ -112,15 +121,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var album = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@ -136,9 +150,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Playlists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@ -149,15 +165,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<Playlist>(itemId, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@ -209,9 +230,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@ -222,11 +245,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -246,9 +274,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Items/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@ -259,11 +289,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -283,9 +318,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Use GetInstantMixFromArtists")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
[FromQuery, Required] Guid id,
@ -320,9 +357,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
@ -333,11 +372,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(id, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -64,7 +66,7 @@ public class ItemLookupController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -234,6 +236,7 @@ public class ItemLookupController : BaseJellyfinApiController
/// <param name="searchResult">The remote search result.</param>
/// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
/// <response code="204">Item metadata refreshed.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
/// The task result contains an <see cref="NoContentResult"/>.
@ -241,12 +244,18 @@ public class ItemLookupController : BaseJellyfinApiController
[HttpPost("Items/RemoteSearch/Apply/{itemId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> ApplySearchCriteria(
[FromRoute, Required] Guid itemId,
[FromBody, Required] RemoteSearchResult searchResult,
[FromQuery] bool replaceAllImages = true)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
}
_logger.LogInformation(
"Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
item.Id,

@ -2,7 +2,10 @@ using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
@ -61,7 +64,7 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] bool replaceAllMetadata = false,
[FromQuery] bool replaceAllImages = false)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();

@ -5,6 +5,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
@ -72,7 +74,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -145,7 +147,11 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
}
var info = new MetadataEditorInfo
{
@ -197,7 +203,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -258,7 +264,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (request.Studios is not null)
{
item.Studios = request.Studios.Select(x => x.Name).ToArray();
item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
}
if (request.DateCreated.HasValue)
@ -282,7 +288,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (item is Series rseries)
{
foreach (Season season in rseries.Children)
foreach (var season in rseries.Children.OfType<Season>())
{
season.OfficialRating = request.OfficialRating;
season.CustomRating = request.CustomRating;
@ -290,7 +296,7 @@ public class ItemUpdateController : BaseJellyfinApiController
season.OnMetadataChanged();
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
foreach (Episode ep in season.Children)
foreach (var ep in season.Children.OfType<Episode>())
{
ep.OfficialRating = request.OfficialRating;
ep.CustomRating = request.CustomRating;
@ -302,7 +308,7 @@ public class ItemUpdateController : BaseJellyfinApiController
}
else if (item is Season season)
{
foreach (Episode ep in season.Children)
foreach (var ep in season.Children.OfType<Episode>())
{
ep.OfficialRating = request.OfficialRating;
ep.CustomRating = request.CustomRating;
@ -373,10 +379,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasAlbumArtist hasAlbumArtists)
{
hasAlbumArtists.AlbumArtists = request
.AlbumArtists
.Select(i => i.Name)
.ToArray();
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
}
}
@ -384,10 +387,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasArtist hasArtists)
{
hasArtists.Artists = request
.ArtistItems
.Select(i => i.Name)
.ToArray();
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
}
}

@ -246,9 +246,9 @@ public class ItemsController : BaseJellyfinApiController
var isApiKey = User.GetIsApiKey();
// if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
userId = RequestHelpers.GetUserId(User, userId);
var user = !isApiKey && !userId.IsNullOrEmpty()
? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
: null;
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException();
// beyond this point, we're either using an api key or we have a valid user
if (!isApiKey && user is null)
@ -256,6 +256,13 @@ public class ItemsController : BaseJellyfinApiController
return BadRequest("userId is required");
}
if (user is not null
&& user.GetPreference(PreferenceKind.AllowedTags).Length != 0
&& !fields.Contains(ItemFields.Tags))
{
fields = [..fields, ItemFields.Tags];
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -967,9 +974,13 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
return _userDataRepository.GetUserDataDto(item, user);
}
/// <summary>
@ -1014,8 +1025,8 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
if (item == null)
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}

@ -102,7 +102,7 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public ActionResult GetFile([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@ -152,20 +152,19 @@ public class LibraryController : BaseJellyfinApiController
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
: _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound("Item not found.");
return NotFound();
}
IEnumerable<BaseItem> themeItems;
IReadOnlyList<BaseItem> themeItems;
while (true)
{
themeItems = item.GetThemeSongs();
if (themeItems.Any() || !inheritFromParent)
if (themeItems.Count > 0 || !inheritFromParent)
{
break;
}
@ -214,16 +213,14 @@ public class LibraryController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = itemId.IsEmpty()
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
: _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound("Item not found.");
return NotFound();
}
IEnumerable<BaseItem> themeItems;
@ -286,7 +283,8 @@ public class LibraryController : BaseJellyfinApiController
userId,
inheritFromParent);
if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
if (themeSongs.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound }
|| themeVideos.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound })
{
return NotFound();
}
@ -327,6 +325,7 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <response code="204">Item deleted.</response>
/// <response code="401">Unauthorized access.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Items/{itemId}")]
[Authorize]
@ -335,17 +334,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItem(Guid itemId)
{
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
var user = !isApiKey && !userId.IsEmpty()
? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
: null;
if (!isApiKey && user is null)
var isApiKey = User.GetIsApiKey();
var user = userId.IsEmpty() && isApiKey
? null
: _userManager.GetUserById(userId);
if (user is null && !isApiKey)
{
return Unauthorized("Unauthorized access");
return NotFound();
}
var item = _libraryManager.GetItemById(itemId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
@ -391,7 +391,7 @@ public class LibraryController : BaseJellyfinApiController
foreach (var i in ids)
{
var item = _libraryManager.GetItemById(i);
var item = _libraryManager.GetItemById<BaseItem>(i, user);
if (item is null)
{
return NotFound();
@ -459,20 +459,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound("Item not found");
return NotFound();
}
var baseItemDtos = new List<BaseItemDto>();
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions().AddClientFields(User);
BaseItem? parent = item.GetParent();
@ -520,7 +518,11 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
{
var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
var items = _libraryManager.GetUserRootFolder().Children
.Concat(_libraryManager.RootFolder.VirtualChildren)
.Where(i => _libraryManager.GetLibraryOptions(i).Enabled)
.OrderBy(i => i.SortName)
.ToList();
if (isHidden.HasValue)
{
@ -640,14 +642,16 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
var userId = User.GetUserId();
var user = userId.IsEmpty()
? null
: _userManager.GetUserById(userId);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
var user = _userManager.GetUserById(User.GetUserId());
if (user is not null)
{
if (!item.CanDownload(user))
@ -700,12 +704,14 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = itemId.IsEmpty()
? (userId.IsNullOrEmpty()
? (user is null
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
: _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
@ -716,9 +722,6 @@ public class LibraryController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>();
}
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User);

@ -6,6 +6,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Api;
@ -73,7 +75,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string? name,
[FromQuery] string name,
[FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
@ -83,7 +85,7 @@ public class LibraryStructureController : BaseJellyfinApiController
if (paths is not null && paths.Length > 0)
{
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
libraryOptions.PathInfos = Array.ConvertAll(paths, i => new MediaPathInfo(i));
}
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
@ -101,7 +103,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveVirtualFolder(
[FromQuery] string? name,
[FromQuery] string name,
[FromQuery] bool refreshLibrary = false)
{
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
@ -265,18 +267,16 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path removed.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
/// <exception cref="ArgumentException">The name of the library and path may not be empty.</exception>
[HttpDelete("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveMediaPath(
[FromQuery] string? name,
[FromQuery] string? path,
[FromQuery] string name,
[FromQuery] string path,
[FromQuery] bool refreshLibrary = false)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name));
}
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
_libraryMonitor.Stop();
@ -311,15 +311,21 @@ public class LibraryStructureController : BaseJellyfinApiController
/// </summary>
/// <param name="request">The library name and options.</param>
/// <response code="204">Library updated.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("LibraryOptions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateLibraryOptions(
[FromBody] UpdateLibraryOptionsDto request)
{
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
var item = _libraryManager.GetItemById<CollectionFolder>(request.Id, User.GetUserId());
if (item is null)
{
return NotFound();
}
collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
item.UpdateLibraryOptions(request.LibraryOptions);
return NoContent();
}
}

@ -220,9 +220,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="channelId">Channel id.</param>
/// <param name="userId">Optional. Attach user data.</param>
/// <response code="200">Live tv channel returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
[HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
@ -232,7 +234,12 @@ public class LiveTvController : BaseJellyfinApiController
: _userManager.GetUserById(userId.Value);
var item = channelId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(channelId);
: _libraryManager.GetItemById<BaseItem>(channelId, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@ -416,9 +423,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="recordingId">Recording id.</param>
/// <param name="userId">Optional. Attach user data.</param>
/// <response code="200">Recording returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
[HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
@ -426,7 +435,13 @@ public class LiveTvController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
var item = recordingId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById<BaseItem>(recordingId, user);
if (item is null)
{
return NotFound();
}
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@ -611,7 +626,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
var series = _libraryManager.GetItemById<Series>(librarySeriesId.Value);
if (series is not null)
{
query.Name = series.Name;
}
@ -665,7 +681,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
var series = _libraryManager.GetItemById<Series>(body.LibrarySeriesId);
if (series is not null)
{
query.Name = series.Name;
}
@ -779,7 +796,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{
var item = _libraryManager.GetItemById(recordingId);
var item = _libraryManager.GetItemById<BaseItem>(recordingId, User.GetUserId());
if (item is null)
{
return NotFound();

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities.Audio;
@ -66,37 +67,16 @@ public class LyricsController : BaseJellyfinApiController
[HttpGet("Audio/{itemId}/Lyrics")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<LyricDto>> GetLyrics([FromRoute, Required] Guid itemId)
{
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
if (!isApiKey && userId.IsEmpty())
{
return BadRequest();
}
var audio = _libraryManager.GetItemById<Audio>(itemId);
if (audio is null)
var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
}
if (!isApiKey)
{
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
// Check the item is visible for the user
if (!audio.IsVisible(user))
{
return Unauthorized($"{user.Username} is not permitted to access item {audio.Name}.");
}
}
var result = await _lyricManager.GetLyricsAsync(audio, CancellationToken.None).ConfigureAwait(false);
var result = await _lyricManager.GetLyricsAsync(item, CancellationToken.None).ConfigureAwait(false);
if (result is not null)
{
return Ok(result);
@ -124,8 +104,8 @@ public class LyricsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery, Required] string fileName)
{
var audio = _libraryManager.GetItemById<Audio>(itemId);
if (audio is null)
var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
}
@ -147,7 +127,7 @@ public class LyricsController : BaseJellyfinApiController
{
await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
var uploadedLyric = await _lyricManager.SaveLyricAsync(
audio,
item,
format,
stream)
.ConfigureAwait(false);
@ -157,7 +137,7 @@ public class LyricsController : BaseJellyfinApiController
return BadRequest();
}
_providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return Ok(uploadedLyric);
}
}
@ -176,13 +156,13 @@ public class LyricsController : BaseJellyfinApiController
public async Task<ActionResult> DeleteLyrics(
[FromRoute, Required] Guid itemId)
{
var audio = _libraryManager.GetItemById<Audio>(itemId);
if (audio is null)
var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
}
await _lyricManager.DeleteLyricsAsync(audio).ConfigureAwait(false);
await _lyricManager.DeleteLyricsAsync(item).ConfigureAwait(false);
return NoContent();
}
@ -199,13 +179,13 @@ public class LyricsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IReadOnlyList<RemoteLyricInfoDto>>> SearchRemoteLyrics([FromRoute, Required] Guid itemId)
{
var audio = _libraryManager.GetItemById<Audio>(itemId);
if (audio is null)
var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
}
var results = await _lyricManager.SearchLyricsAsync(audio, false, CancellationToken.None).ConfigureAwait(false);
var results = await _lyricManager.SearchLyricsAsync(item, false, CancellationToken.None).ConfigureAwait(false);
return Ok(results);
}
@ -225,19 +205,19 @@ public class LyricsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string lyricId)
{
var audio = _libraryManager.GetItemById<Audio>(itemId);
if (audio is null)
var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
}
var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(audio, lyricId, CancellationToken.None).ConfigureAwait(false);
var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(item, lyricId, CancellationToken.None).ConfigureAwait(false);
if (downloadedLyrics is null)
{
return NotFound();
}
_providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return Ok(downloadedLyrics);
}

@ -8,8 +8,10 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
@ -32,6 +34,7 @@ public class MediaInfoController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly ILogger<MediaInfoController> _logger;
private readonly MediaInfoHelper _mediaInfoHelper;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
@ -41,18 +44,21 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
/// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface..</param>
public MediaInfoController(
IMediaSourceManager mediaSourceManager,
IDeviceManager deviceManager,
ILibraryManager libraryManager,
ILogger<MediaInfoController> logger,
MediaInfoHelper mediaInfoHelper)
MediaInfoHelper mediaInfoHelper,
IUserManager userManager)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
_logger = logger;
_mediaInfoHelper = mediaInfoHelper;
_userManager = userManager;
}
/// <summary>
@ -61,16 +67,24 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
/// <response code="200">Playback info returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
[HttpGet("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
return await _mediaInfoHelper.GetPlaybackInfo(
itemId,
userId)
.ConfigureAwait(false);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
return await _mediaInfoHelper.GetPlaybackInfo(item, user).ConfigureAwait(false);
}
/// <summary>
@ -97,9 +111,11 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
/// <param name="playbackInfoDto">The playback info.</param>
/// <response code="200">Playback info returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
[HttpPost("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId,
[FromQuery, ParameterObsolete] Guid? userId,
@ -148,9 +164,19 @@ public class MediaInfoController : BaseJellyfinApiController
allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
var info = await _mediaInfoHelper.GetPlaybackInfo(
itemId,
userId,
item,
user,
mediaSourceId,
liveStreamId)
.ConfigureAwait(false);
@ -163,8 +189,6 @@ public class MediaInfoController : BaseJellyfinApiController
if (profile is not null)
{
// set device specific data
var item = _libraryManager.GetItemById(itemId);
foreach (var mediaSource in info.MediaSources)
{
_mediaInfoHelper.SetDeviceSpecificData(

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save