Merge branch 'master' into sort-nfo-data

pull/9560/head
Marc Brooks 1 month ago committed by GitHub
commit a5f3d942f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11

@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: abi-head
retention-days: 14
@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: abi-base
retention-days: 14
@ -85,13 +85,13 @@ jobs:
steps:
- name: Download abi-head
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: abi-head
path: abi-head
- name: Download abi-base
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: abi-base
path: abi-base

@ -27,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@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: openapi-head
retention-days: 14
@ -61,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@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: openapi-base
retention-days: 14
@ -80,12 +80,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: openapi-base
path: openapi-base
@ -158,7 +158,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: openapi-head
path: openapi-head
@ -172,7 +172,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@ -220,7 +220,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
name: openapi-head
path: openapi-head
@ -234,7 +234,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"

@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@c38c522d4b391c1b0da979cbb2e902c0a252a7dc # v5.4.3
uses: danielpalme/ReportGenerator-GitHub-Action@f1927db1dbfc029b056583ee488832e939447fe6 # v5.4.4
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"

@ -34,94 +34,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
check-backport:
permissions:
contents: read
name: Check Backport
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Running backport tests...
- name: Perform test backport
id: run_tests
run: |
set +o errexit
git config --global user.name "Jellyfin Bot"
git config --global user.email "team@jellyfin.org"
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
git checkout master
git merge --no-ff ${CURRENT_BRANCH}
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
git fetch --all
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
echo ${stable_branch}
echo ::set-output name=branch::${stable_branch}
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
retcode=$?
cat output.txt | grep -v 'hint:'
output="$( grep -v 'hint:' output.txt )"
output="${output//'%'/'%25'}"
output="${output//$'\n'/'%0A'}"
output="${output//$'\r'/'%0D'}"
echo ::set-output name=output::$output
exit ${retcode}
- name: Notify with result success
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ steps.comment_running.outputs.comment-id }}
body: |
${{ steps.run_tests.outputs.branch }}
Output from `git cherry-pick`:
---
${{ steps.run_tests.outputs.output }}
reactions: hooray
- name: Notify with result failure
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ steps.comment_running.outputs.comment-id }}
body: |
${{ steps.run_tests.outputs.branch }}
Output from `git cherry-pick`:
---
${{ steps.run_tests.outputs.output }}
reactions: confused
rename:
name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'

@ -16,38 +16,38 @@
<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="FsCheck.Xunit" Version="3.0.1" />
<PackageVersion Include="FsCheck.Xunit" Version="3.1.0" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.10" />
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="LrcParser" Version="2025.228.1" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.12.0" />
@ -75,11 +75,11 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.1" />
<PackageVersion Include="System.Text.Json" Version="9.0.1" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
<PackageVersion Include="System.Text.Json" Version="9.0.2" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="6.15.0" />
<PackageVersion Include="z440.atl.core" Version="6.19.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />

@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
/// Gets the path to the program data folder.
/// </summary>
/// <value>The program data path.</value>
/// <inheritdoc/>
public string ProgramDataPath { get; }
/// <inheritdoc/>
public string WebPath { get; }
/// <summary>
/// Gets the path to the system folder.
/// </summary>
/// <value>The path to the system folder.</value>
/// <inheritdoc/>
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
/// <summary>
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
/// <inheritdoc/>
public string DataPath { get; }
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
/// <summary>
/// Gets the image cache path.
/// </summary>
/// <value>The image cache path.</value>
/// <inheritdoc/>
public string ImageCachePath => Path.Combine(CachePath, "images");
/// <summary>
/// Gets the path to the plugin directory.
/// </summary>
/// <value>The plugins path.</value>
/// <inheritdoc/>
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
/// <summary>
/// Gets the path to the plugin configurations directory.
/// </summary>
/// <value>The plugin configurations path.</value>
/// <inheritdoc/>
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
/// <summary>
/// Gets the path to the log directory.
/// </summary>
/// <value>The log directory path.</value>
/// <inheritdoc/>
public string LogDirectoryPath { get; }
/// <summary>
/// Gets the path to the application configuration root directory.
/// </summary>
/// <value>The configuration directory path.</value>
/// <inheritdoc/>
public string ConfigurationDirectoryPath { get; }
/// <summary>
/// Gets the path to the system configuration file.
/// </summary>
/// <value>The system configuration file path.</value>
/// <inheritdoc/>
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
/// <summary>
/// Gets or sets the folder path to the cache directory.
/// </summary>
/// <value>The cache directory.</value>
/// <inheritdoc/>
public string CachePath { get; set; }
/// <summary>
/// Gets the folder path to the temp directory within the cache folder.
/// </summary>
/// <value>The temp directory.</value>
/// <inheritdoc/>
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
/// <inheritdoc />
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
}
}

@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.Collections
{
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied Id");
throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
}
List<BaseItem>? itemList = null;
@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Collections
if (item is null)
{
throw new ArgumentException("No item exists with the supplied Id");
throw new ArgumentException("No item exists with the supplied Id " + id);
}
if (!currentLinkedChildrenIds.Contains(id))

@ -457,6 +457,7 @@ namespace Emby.Server.Implementations.Library
foreach (var child in children)
{
_itemRepository.DeleteItem(child.Id);
_cache.TryRemove(child.Id, out _);
}
_cache.TryRemove(item.Id, out _);
@ -1811,11 +1812,11 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public void CreateItem(BaseItem item, BaseItem? parent)
{
CreateOrUpdateItems(new[] { item }, parent, CancellationToken.None);
CreateItems(new[] { item }, parent, CancellationToken.None);
}
/// <inheritdoc />
public void CreateOrUpdateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
{
_itemRepository.SaveItems(items, cancellationToken);
@ -2630,15 +2631,6 @@ namespace Emby.Server.Implementations.Library
{
episode.ParentIndexNumber = season.IndexNumber;
}
else
{
/*
Anime series don't generally have a season in their file name, however,
TVDb needs a season to correctly get the metadata.
Hence, a null season needs to be filled with something. */
// FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
episode.ParentIndexNumber = 1;
}
if (episode.ParentIndexNumber.HasValue)
{
@ -2972,11 +2964,11 @@ namespace Emby.Server.Implementations.Library
{
if (createEntity)
{
CreateOrUpdateItems([personEntity], null, CancellationToken.None);
CreateItems([personEntity], null, CancellationToken.None);
}
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
CreateOrUpdateItems([personEntity], null, CancellationToken.None);
CreateItems([personEntity], null, CancellationToken.None);
}
}
}

@ -43,14 +43,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
/// <inheritdoc />
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
var posters = GetItemsWithImageType(ImageType.Primary)
.Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
var backdrops = GetItemsWithImageType(ImageType.Thumb)
.Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
if (backdrops.Count == 0)
{
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
// Using backdrops as a fallback to generate an image at all
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
backdrops = GetItemsWithImageType(ImageType.Backdrop)
.Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
}
_imageEncoder.CreateSplashscreen(posters, backdrops);

@ -134,5 +134,7 @@
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
"TaskDownloadMissingLyricsDescription": "كلمات",
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
"TaskExtractMediaSegmentsDescription": "وسائط"
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
}

@ -1,6 +1,6 @@
{
"Sync": "Сінхранізаваць",
"Playlists": "Плэйлісты",
"Playlists": "Спісы прайгравання",
"Latest": "Апошні",
"LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
@ -16,7 +16,7 @@
"Collections": "Калекцыі",
"Default": "Па змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі",
"Folders": "Тэчкі",
"Favorites": "Абранае",
"External": "Знешні",
"Genres": "Жанры",

@ -16,7 +16,7 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum",
"HeaderContinueWatching": "Continuar veient",
"HeaderContinueWatching": "Continua veient",
"HeaderFavoriteAlbums": "Àlbums preferits",
"HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis preferits",
@ -24,13 +24,13 @@
"HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups d'enregistrament",
"HeaderRecordingGroups": "Grups Musicals",
"HomeVideos": "Vídeos domèstics",
"Inherit": "Hereta",
"ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
"ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
"Inherit": "Heretat",
"ItemAddedWithName": "{0} s'ha afegit a la biblioteca",
"ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca",
"LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps en funcionament: {0}",
"LabelRunningTimeValue": "Temps en marxa: {0}",
"Latest": "Darrers",
"MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
@ -44,8 +44,8 @@
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconeguda",
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
@ -54,8 +54,8 @@
"NotificationOptionPluginError": "Un complement ha fallat",
"NotificationOptionPluginInstalled": "Complement instal·lat",
"NotificationOptionPluginUninstalled": "Complement desinstal·lat",
"NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
"NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
"NotificationOptionPluginUpdateInstalled": "Actualització del complement instal·lada",
"NotificationOptionServerRestartRequired": "El servidor s'ha de reiniciar",
"NotificationOptionTaskFailed": "Tasca programada fallida",
"NotificationOptionUserLockedOut": "Usuari expulsat",
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
@ -64,15 +64,15 @@
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "{0} ha estat desinstal·lat",
"PluginUpdatedWithName": "{0} ha estat actualitzat",
"PluginUninstalledWithName": "S'ha instalat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
"ScheduledTaskStartedWithName": "{0} s'ha iniciat",
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
"ScheduledTaskStartedWithName": "S'ha iniciat {0}",
"ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
"Shows": "Sèries",
"Songs": "Cançons",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitzar",
@ -80,41 +80,41 @@
"TvShows": "Sèries de TV",
"User": "Usuari",
"UserCreatedWithName": "S'ha creat l'usuari {0}",
"UserDeletedWithName": "L'usuari {0} ha estat eliminat",
"UserDeletedWithName": "S'ha eliminat l'usuari {0}",
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
"UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
"UserLockedOutWithName": "S'ha expulsat a l'usuari {0}",
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}",
"UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
"UserPasswordChangedWithName": "S'ha canviat la contrasenya per a l'usuari {0}",
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
"ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
"TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja les transcodificacions",
"TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualitza els connectors",
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
"TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualitza els complements",
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.",
"TaskRefreshPeople": "Actualitza les persones",
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja els registres",
"TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
"TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
"TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
"TaskCleanCache": "Elimina arxius temporals",
"TasksChannelsCategory": "Canals d'internet",
"TasksApplicationCategory": "Aplicació",
"TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
"TaskCleanCache": "Elimina la memòria cau",
"TasksChannelsCategory": "Canals per internet",
"TasksApplicationCategory": "Aplicatiu",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manteniment",
"TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
"TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buidar el registre d'activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
@ -128,11 +128,11 @@
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
"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.",
"TaskDownloadMissingLyricsDescription": "Baixar lletres de les cançons",
"TaskDownloadMissingLyrics": "Baixar lletres que falten",
"TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
"TaskAudioNormalization": "Estabilització d'Àudio",
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.",
"TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons",
"TaskDownloadMissingLyrics": "Baixar les lletres que falten",
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
"TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",

@ -11,7 +11,7 @@
"Collections": "Συλλογές",
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
"FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}",
"FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
@ -28,7 +28,7 @@
"HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση",
"ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
"ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη",
"ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
"LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα",
@ -96,7 +96,7 @@
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
"TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
"TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
"TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
"TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων",
"TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
"TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
"TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",

@ -19,25 +19,25 @@
"Artists": "Artistak",
"Albums": "Albumak",
"TaskOptimizeDatabase": "Datu basea optimizatu",
"TaskDownloadMissingSubtitlesDescription": "Metadataren konfigurazioan oinarrituta falta diren azpitituluak bilatzen ditu interneten.",
"TaskDownloadMissingSubtitlesDescription": "Falta diren azpitituluak bilatzen ditu interneten metadatuen konfigurazioaren arabera.",
"TaskDownloadMissingSubtitles": "Falta diren azpitituluak deskargatu",
"TaskRefreshChannelsDescription": "Internet kanalen informazioa eguneratu.",
"TaskRefreshChannels": "Kanalak eguneratu",
"TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transcode fitxategiak ezabatzen ditu.",
"TaskCleanTranscode": "Transcode direktorioa garbitu",
"TaskUpdatePluginsDescription": "Automatikoki eguneratzeko konfiguratutako pluginen eguneraketak deskargatu eta instalatzen ditu.",
"TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transkodifikazio fitxategiak ezabatzen ditu.",
"TaskCleanTranscode": "Transkodifikazio direktorioa garbitu",
"TaskUpdatePluginsDescription": "Automatikoki deskargatu eta instalatu eguneraketak konfiguratutako pluginetarako.",
"TaskUpdatePlugins": "Pluginak eguneratu",
"TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadata eguneratzen du.",
"TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadatuak eguneratzen ditu.",
"TaskRefreshPeople": "Jendea eguneratu",
"TaskCleanLogsDescription": "{0} egun baino zaharragoak diren log fitxategiak ezabatzen ditu.",
"TaskCleanLogs": "Log direktorioa garbitu",
"TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatak eguneratzeko.",
"TaskRefreshLibrary": "Multimedia Liburutegia eskaneatu",
"TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatuak eguneratzeko.",
"TaskRefreshLibrary": "Multimedia liburutegia eskaneatu",
"TaskRefreshChapterImagesDescription": "Kapituluak dituzten bideoen miniaturak sortzen ditu.",
"TaskRefreshChapterImages": "Kapituluen irudiak erauzi",
"TaskCleanCacheDescription": "Sistemak behar ez dituen cache fitxategiak ezabatzen ditu.",
"TaskCleanCache": "Cache Directorioa garbitu",
"TaskCleanActivityLogDescription": "Konfiguratuta data baino zaharragoak diren log-ak ezabatu.",
"TaskCleanCache": "Cache direktorioa garbitu",
"TaskCleanActivityLogDescription": "Konfiguratutako baino zaharragoak diren jarduera-log sarrerak ezabatzen ditu.",
"TaskCleanActivityLog": "Erabilera Log-a garbitu",
"TasksChannelsCategory": "Internet Kanalak",
"TasksApplicationCategory": "Aplikazioa",
@ -45,22 +45,22 @@
"TasksMaintenanceCategory": "Mantenua",
"VersionNumber": "Bertsioa {0}",
"ValueHasBeenAddedToLibrary": "{0} zure multimedia liburutegian gehitu da",
"UserStoppedPlayingItemWithValues": "{0}-ek {1} ikusteaz bukatu du {2}-(a)n",
"UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(a)n",
"UserPolicyUpdatedWithName": "{0} Erabiltzailearen politikak aldatu dira",
"UserPasswordChangedWithName": "{0} Erabiltzailearen pasahitza aldatu da",
"UserOnlineFromDevice": "{0} online dago {1}-tik",
"UserOfflineFromDevice": "{0} {1}-tik deskonektatu da",
"UserLockedOutWithName": "{0} Erabiltzailea blokeatu da",
"UserDownloadingItemWithValues": "{1} {0}-tik deskargatzen",
"UserStoppedPlayingItemWithValues": "{0} {1} ikusten bukatu du {2}-(e)n",
"UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(e)n",
"UserPolicyUpdatedWithName": "{0} erabiltzailearen politikak aldatu dira",
"UserPasswordChangedWithName": "{0} erabiltzailearen pasahitza aldatu da",
"UserOnlineFromDevice": "{0} online dago {1}-(e)tik",
"UserOfflineFromDevice": "{0} {1}-(e)tik deskonektatu da",
"UserLockedOutWithName": "{0} erabiltzailea blokeatu da",
"UserDownloadingItemWithValues": "{0} {1} deskargatzen ari da",
"UserDeletedWithName": "{0} Erabiltzailea ezabatu da",
"UserCreatedWithName": "{0} Erabiltzailea sortu da",
"User": "Erabiltzailea",
"Undefined": "Ezezaguna",
"TvShows": "TB showak",
"TvShows": "TB serieak",
"System": "Sistema",
"SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0} deskargatzean huts egin du",
"StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduxeago.",
"SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0}-tik deskargatzeak huts egin du",
"StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduago.",
"ServerNameNeedsToBeRestarted": "{0} berrabiarazi behar da",
"ScheduledTaskStartedWithName": "{0} hasi da",
"ScheduledTaskFailedWithName": "{0} huts egin du",
@ -89,26 +89,26 @@
"NameSeasonNumber": "{0} Denboraldia",
"NameInstallFailed": "{0} instalazioak huts egin du",
"Music": "Musika",
"MixedContent": "Denetariko edukia",
"MixedContent": "Eduki mistoa",
"MessageServerConfigurationUpdated": "Zerbitzariaren konfigurazioa eguneratu da",
"MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren konfigurazio {0} atala eguneratu da",
"MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren {0} konfigurazio atala eguneratu da",
"MessageApplicationUpdatedTo": "Jellyfin zerbitzaria {0}-ra eguneratu da",
"MessageApplicationUpdated": "Jellyfin zerbitzaria eguneratu da",
"Latest": "Azkena",
"LabelRunningTimeValue": "Denbora martxan: {0}",
"LabelRunningTimeValue": "Iraupena: {0}",
"LabelIpAddressValue": "IP helbidea: {0}",
"ItemRemovedWithName": "{0} liburutegitik ezabatu da",
"ItemRemovedWithName": "{0} liburutegitik kendu da",
"ItemAddedWithName": "{0} liburutegira gehitu da",
"HomeVideos": "Etxeko bideoak",
"HeaderNextUp": "Nobedadeak",
"HeaderNextUp": "Hurrengoa",
"HeaderLiveTV": "Zuzeneko TB",
"HeaderFavoriteSongs": "Gogoko abestiak",
"HeaderFavoriteShows": "Gogoko showak",
"HeaderFavoriteShows": "Gogoko serieak",
"HeaderFavoriteEpisodes": "Gogoko atalak",
"HeaderFavoriteArtists": "Gogoko artistak",
"HeaderFavoriteAlbums": "Gogoko albumak",
"Forced": "Behartuta",
"FailedLoginAttemptWithUserName": "Login egiten akatsa, saiatu hemen {0}",
"FailedLoginAttemptWithUserName": "{0}-tik saioa hasteak huts egin du",
"External": "Kanpokoa",
"DeviceOnlineWithName": "{0} konektatu da",
"DeviceOfflineWithName": "{0} deskonektatu da",
@ -117,13 +117,23 @@
"AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
"Application": "Aplikazioa",
"AppDeviceValues": "App: {0}, Gailua: {1}",
"HearingImpaired": "Entzunaldia aldatua",
"HearingImpaired": "Entzumen urritasuna",
"ProviderValue": "Hornitzailea: {0}",
"TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
"HeaderRecordingGroups": "Grabaketa taldeak",
"Inherit": "Oinordetu",
"TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
"TaskKeyframeExtractor": "Fotograma gakoen erauzgailua",
"TaskRefreshTrickplayImages": "\"Trickplay Irudiak Sortu",
"TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan."
"TaskRefreshTrickplayImages": "Trickplay irudiak sortu",
"TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan.",
"TaskAudioNormalization": "Audio normalizazioa",
"TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
"TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
"TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa",
"TaskCleanCollectionsAndPlaylistsDescription": "Jada existitzen ez diren bildumak eta erreprodukzio-zerrendak kentzen ditu.",
"TaskCleanCollectionsAndPlaylists": "Garbitu bildumak eta erreprodukzio-zerrendak",
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
}

@ -13,7 +13,7 @@
"DeviceOnlineWithName": "{0} belépett",
"FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
"Favorites": "Kedvencek",
"Folders": "Könyvtárak",
"Folders": "Mappák",
"Genres": "Műfajok",
"HeaderAlbumArtists": "Albumelőadók",
"HeaderContinueWatching": "Megtekintés folytatása",

@ -58,8 +58,8 @@
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
"NotificationOptionTaskFailed": "Operazione pianificata fallita",
"NotificationOptionUserLockedOut": "Utente bloccato",
"NotificationOptionVideoPlayback": "La riproduzione video è iniziata",
"NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta",
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto",
"Playlists": "Playlist",
"Plugin": "Plugin",

@ -0,0 +1,139 @@
{
"Albums": "Alben",
"Application": "Applikatioun",
"Artists": "Kënschtler",
"Books": "Bicher",
"Channels": "Kanäl",
"Collections": "Kollektiounen",
"Default": "Standard",
"ChapterNameValue": "Kapitel {0}",
"DeviceOnlineWithName": "{0} ass Online",
"DeviceOfflineWithName": "{0} ass Offline",
"External": "Extern",
"Favorites": "Favoritten",
"Folders": "Dossieren",
"Forced": "Forcéiert",
"HeaderAlbumArtists": "Album Kënschtler",
"HeaderFavoriteAlbums": "Léifsten Alben",
"HeaderFavoriteArtists": "Léifsten Kënschtler",
"HeaderFavoriteEpisodes": "Léifsten Episoden",
"HeaderFavoriteShows": "Léifsten Shows",
"HeaderFavoriteSongs": "Léifsten Lidder",
"Genres": "Generen",
"HeaderContinueWatching": "Weider kucken",
"Inherit": "Iwwerhuelen",
"HeaderNextUp": "Als Nächst",
"HeaderRecordingGroups": "Opname Gruppen",
"HearingImpaired": "Daaf",
"HomeVideos": "Amateur Videoen",
"ItemRemovedWithName": "Element ewech geholl: {0}",
"LabelIpAddressValue": "IP Adress: {0}",
"LabelRunningTimeValue": "Lafzäit: {0}",
"Latest": "Dat Aktuellst",
"MessageApplicationUpdatedTo": "Jellyfin Server aktualiséiert op {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server Konfiguratiounssektioun {0} aktualiséiert",
"MessageServerConfigurationUpdated": "Server Konfiguratioun aktualiséiert",
"Movies": "Filmer",
"Music": "Musek",
"NameInstallFailed": "{0} Installatioun net gelongen",
"NameSeasonNumber": "Staffel {0}",
"NameSeasonUnknown": "Staffel Onbekannt",
"MusicVideos": "Museksvideoen",
"NotificationOptionApplicationUpdateAvailable": "Applikatiouns Update verfügbar",
"NotificationOptionApplicationUpdateInstalled": "Applikatiouns Update nët Installéiert",
"NotificationOptionAudioPlayback": "Audio ofspillen gestart",
"NotificationOptionAudioPlaybackStopped": "Audio ofspillen gestoppt",
"NotificationOptionCameraImageUploaded": "Kamera Bild eropgelueden",
"NotificationOptionInstallationFailed": "Installatioun net gelongen",
"NotificationOptionNewLibraryContent": "Neien Bibliothéik Inhalt",
"NotificationOptionPluginError": "Plugin Feeler",
"NotificationOptionPluginInstalled": "Plugin installéiert",
"NotificationOptionPluginUninstalled": "Plugin desinstalléiert",
"NotificationOptionPluginUpdateInstalled": "Plugin Update installéiert",
"Photos": "Fotoen",
"NotificationOptionTaskFailed": "Aufgab net gelongen",
"NotificationOptionUserLockedOut": "Benotzer Gesperrt",
"NotificationOptionVideoPlaybackStopped": "Video ofspillen gestoppt",
"NotificationOptionVideoPlayback": "Video ofspillen gestartet",
"Plugin": "Plugin",
"PluginUninstalledWithName": "{0} desinstalléiert",
"PluginUpdatedWithName": "{0} aktualiséiert",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "Aufgab: {0} net gelongen",
"Playlists": "Playlëschten",
"Shows": "Shows",
"Songs": "Lidder",
"ServerNameNeedsToBeRestarted": "{0} muss nei gestart ginn",
"StartupEmbyServerIsLoading": "Jellyfin Server luedt. Probéier méi spéit nach eng Kéier.",
"Sync": "Synchroniséieren",
"System": "System",
"User": "Benotzer",
"TvShows": "TV Shows",
"Undefined": "Net definéiert",
"UserCreatedWithName": "Benotzer {0} erstellt",
"UserDownloadingItemWithValues": "{0} luet {1} erof",
"UserOfflineFromDevice": "{0} Benotzer Offline um Gerät {1}",
"UserLockedOutWithName": "Benotzer {0} gesperrt",
"UserOnlineFromDevice": "{0} Benotzer Online um Gerät {1}",
"UserPasswordChangedWithName": "Benotzer Passwuert geännert fir {0}",
"UserPolicyUpdatedWithName": "Benotzer Politik aktualiséiert fir: {0}",
"UserStartedPlayingItemWithValues": "{0} spillt {1} op {2} oof",
"ValueHasBeenAddedToLibrary": "{0} der Bibliothéik bäigefüügt",
"VersionNumber": "Versioun {0}",
"TasksMaintenanceCategory": "Ënnerhalt",
"TasksLibraryCategory": "Bibliothéik",
"ValueSpecialEpisodeName": "Spezial-Episodenumm",
"TasksChannelsCategory": "Internet Kanäl",
"TaskCleanActivityLog": "Aktivitéits Log botzen",
"TaskCleanActivityLogDescription": "Läscht Aktivitéitslogs méi al wéi konfiguréiert.",
"TaskCleanCache": "Aufgab Cache Botzen",
"TaskRefreshChapterImages": "Kapitel Biller erstellen",
"TaskRefreshChapterImagesDescription": "Erstellt Miniaturbiller fir Videoen, déi Kapitelen hunn.",
"TaskAudioNormalization": "Audio Normaliséierung",
"TaskRefreshLibrary": "Bibliothéik aktualiséieren",
"TaskRefreshLibraryDescription": "Scannt deng Mediebibliothéik no neien Dateien a frëscht dMetadata op.",
"TaskCleanLogs": "Log Dateien botzen",
"TaskRefreshPeople": "Persounen aktualiséieren",
"TaskRefreshPeopleDescription": "Aktualiséiert Metadata fir Schauspiller a Regisseuren an denger Mediebibliothéik.",
"TaskRefreshTrickplayImagesDescription": "Erstellt Trickplay-Viraussiichten fir Videoen an aktivéierte Bibliothéiken.",
"TaskCleanTranscode": "Transkodéieren botzen",
"TaskCleanTranscodeDescription": "Läscht Transkodéierungsdateien, déi méi al wéi een Dag sinn.",
"TaskRefreshChannels": "Kanäl aktualiséieren",
"TaskDownloadMissingLyrics": "Fehlend Liddertexter eroflueden",
"TaskDownloadMissingLyricsDescription": "Lued Liddertexter fir Lidder erof",
"TaskDownloadMissingSubtitles": "Fehlend Ënnertitelen eroflueden",
"TaskOptimizeDatabase": "Datebank optiméieren",
"TaskKeyframeExtractor": "Schlësselbild Extrakter",
"TaskCleanCollectionsAndPlaylists": "Sammlungen a Playlisten botzen",
"TaskCleanCollectionsAndPlaylistsDescription": "Ewechhuele vun Elementer aus Sammlungen a Playlisten, déi net méi existéieren.",
"TaskExtractMediaSegments": "Mediesegment-Scan",
"NewVersionIsAvailable": "Nei Versioun fir Jellyfin Server ass verfügbar.",
"CameraImageUploadedFrom": "En neit Kamera Bild gouf vu {0} eropgelueden",
"PluginInstalledWithName": "{0} installéiert",
"TaskMoveTrickplayImagesDescription": "Verschëfft existent Trickplay-Dateien no de Bibliothéik-Astellungen.",
"AppDeviceValues": "App: {0}, Geräter: {1}",
"FailedLoginAttemptWithUserName": "Net Gelongen Umeldung {0}",
"HeaderLiveTV": "LiveTV",
"ItemAddedWithName": "Element derbäi gesat: {0}",
"NotificationOptionServerRestartRequired": "Server Restart Erfuerderlech",
"ScheduledTaskStartedWithName": "Aufgab: {0} gestart",
"AuthenticationSucceededWithUserName": "{0} Authentifikatioun gelongen",
"MixedContent": "Gemëschten Inhalt",
"MessageApplicationUpdated": "Jellyfin Server Aktualiséiert",
"SubtitleDownloadFailureFromForItem": "Ënnertitel Download Feeler vun {0} fir {1}",
"TaskCleanLogsDescription": "Läscht Log-Dateien, déi méi al wéi {0} Deeg sinn.",
"TaskUpdatePlugins": "Plugins aktualiséieren",
"UserDeletedWithName": "Benotzer {0} geläscht",
"TasksApplicationCategory": "Applikatioun",
"TaskCleanCacheDescription": "Läscht Cache-Dateien, déi net méi vum System gebraucht ginn.",
"UserStoppedPlayingItemWithValues": "{0} ass mat {1} op {2} fäerdeg",
"TaskAudioNormalizationDescription": "Scannt Dateien no Donnéeën fir dAudio-Normaliséierung.",
"TaskRefreshTrickplayImages": "Trickplay-Biller generéieren",
"TaskDownloadMissingSubtitlesDescription": "Sicht am Internet no fehlenden Ënnertitelen op Basis vun der Metadata-Konfiguratioun.",
"TaskMoveTrickplayImages": "Trickplay-Biller-Plaz migréieren",
"TaskUpdatePluginsDescription": "Lued Aktualiséierungen erof a installéiert se fir Plugins, déi fir automatesch Updates konfiguréiert sinn.",
"TaskKeyframeExtractorDescription": "Extrahéiert Schlësselbiller aus Videodateien, fir méi präzis HLS-Playlisten ze erstellen. Dës Aufgab kann eng längere Zäit daueren.",
"TaskRefreshChannelsDescription": "Aktualiséiert Informatiounen iwwer Internetkanäl.",
"TaskExtractMediaSegmentsDescription": "Extrahéiert oder kritt Mediesegmenter aus Plugins, déi MediaSegment ënnerstëtzen.",
"TaskOptimizeDatabaseDescription": "Kompriméiert dDatebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann dPerformance verbesseren."
}

@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
}
// 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);
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase)
.Trim();
// Use rating system matching the language
if (!string.IsNullOrEmpty(countryCode))

@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Playlists
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (item is null)
{
_logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
_logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId);
return;
}

@ -116,6 +116,7 @@ public partial class AudioNormalizationTask : IScheduledTask
{
a.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
cancellationToken).ConfigureAwait(false);
}
finally
@ -142,7 +143,10 @@ public partial class AudioNormalizationTask : IScheduledTask
continue;
}
t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false);
t.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
false,
cancellationToken).ConfigureAwait(false);
}
_itemRepository.SaveItems(tracks, cancellationToken);
@ -162,7 +166,7 @@ public partial class AudioNormalizationTask : IScheduledTask
];
}
private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
{
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
@ -189,18 +193,28 @@ public partial class AudioNormalizationTask : IScheduledTask
}
using var reader = process.StandardError;
float? lufs = null;
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
{
Match match = LUFSRegex().Match(line);
if (match.Success)
{
return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
break;
}
}
_logger.LogError("Failed to find LUFS value in output");
return null;
if (lufs is null)
{
_logger.LogError("Failed to find LUFS value in output");
}
if (waitForExit)
{
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
}
return lufs;
}
}
}

@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session
private readonly SessionInfo _session;
private readonly List<IWebSocketConnection> _sockets;
private readonly ReaderWriterLockSlim _socketsLock;
private bool _disposed = false;
public WebSocketController(
@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_session = session;
_sessionManager = sessionManager;
_sockets = new List<IWebSocketConnection>();
_sockets = new();
_socketsLock = new();
}
private bool HasOpenSockets => GetActiveSockets().Any();
private bool HasOpenSockets
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
_socketsLock.EnterReadLock();
return _sockets.Any(i => i.State == WebSocketState.Open);
}
finally
{
_socketsLock.ExitReadLock();
}
}
}
/// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets;
@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public bool IsSessionActive => HasOpenSockets;
private IEnumerable<IWebSocketConnection> GetActiveSockets()
=> _sockets.Where(i => i.State == WebSocketState.Open);
public void AddWebSocket(IWebSocketConnection connection)
{
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
_sockets.Add(connection);
connection.Closed += OnConnectionClosed;
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
_socketsLock.EnterWriteLock();
_sockets.Add(connection);
connection.Closed += OnConnectionClosed;
}
finally
{
_socketsLock.ExitWriteLock();
}
}
private async void OnConnectionClosed(object? sender, EventArgs e)
{
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
_sockets.Remove(connection);
connection.Closed -= OnConnectionClosed;
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
_socketsLock.EnterWriteLock();
_sockets.Remove(connection);
connection.Closed -= OnConnectionClosed;
}
finally
{
_socketsLock.ExitWriteLock();
}
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
}
@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session
T data,
CancellationToken cancellationToken)
{
var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate);
ObjectDisposedException.ThrowIf(_disposed, this);
IWebSocketConnection? socket;
try
{
_socketsLock.EnterReadLock();
socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate);
}
finally
{
_socketsLock.ExitReadLock();
}
if (socket is null)
{
@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session
return;
}
foreach (var socket in _sockets)
try
{
_socketsLock.EnterWriteLock();
foreach (var socket in _sockets)
{
socket.Closed -= OnConnectionClosed;
socket.Dispose();
}
_sockets.Clear();
}
finally
{
socket.Closed -= OnConnectionClosed;
socket.Dispose();
_socketsLock.ExitWriteLock();
}
_socketsLock.Dispose();
_disposed = true;
}
@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session
return;
}
foreach (var socket in _sockets)
try
{
_socketsLock.EnterWriteLock();
foreach (var socket in _sockets)
{
socket.Closed -= OnConnectionClosed;
await socket.DisposeAsync().ConfigureAwait(false);
}
_sockets.Clear();
}
finally
{
socket.Closed -= OnConnectionClosed;
await socket.DisposeAsync().ConfigureAwait(false);
_socketsLock.ExitWriteLock();
}
_socketsLock.Dispose();
_disposed = true;
}
}

@ -50,20 +50,21 @@ namespace Jellyfin.Api.Auth
}
var role = UserRoles.User;
if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
if (authorizationInfo.IsApiKey
|| (authorizationInfo.User?.HasPermission(PermissionKind.IsAdministrator) ?? false))
{
role = UserRoles.Administrator;
}
var claims = new[]
{
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
new Claim(ClaimTypes.Role, role),
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId ?? string.Empty),
new Claim(InternalClaimTypes.Device, authorizationInfo.Device ?? string.Empty),
new Claim(InternalClaimTypes.Client, authorizationInfo.Client ?? string.Empty),
new Claim(InternalClaimTypes.Version, authorizationInfo.Version ?? string.Empty),
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
};

@ -91,31 +91,31 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@ -295,31 +295,31 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{

@ -121,10 +121,10 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -197,9 +197,9 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] channelIds)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()

@ -50,7 +50,7 @@ public class CollectionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] ids,
[FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false)
{
@ -86,7 +86,7 @@ public class CollectionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToCollection(
[FromRoute, Required] Guid collectionId,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
return NoContent();
@ -103,7 +103,7 @@ public class CollectionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveFromCollection(
[FromRoute, Required] Guid collectionId,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
return NoContent();

@ -50,8 +50,8 @@ public class FilterController : BaseJellyfinApiController
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -137,7 +137,7 @@ public class FilterController : BaseJellyfinApiController
public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isAiring,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSports,

@ -76,18 +76,18 @@ public class GenresController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{

@ -73,11 +73,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -117,11 +117,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -161,11 +161,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -203,11 +203,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -241,11 +241,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -285,11 +285,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -330,11 +330,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
return GetInstantMixFromArtists(
id,
@ -368,11 +368,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()

@ -171,8 +171,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@ -190,42 +190,42 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@ -236,12 +236,12 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@ -638,8 +638,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@ -657,42 +657,42 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@ -703,12 +703,12 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
=> GetItems(
@ -827,13 +827,13 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true,
[FromQuery] bool excludeActiveSessions = false)
@ -929,13 +929,13 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true,
[FromQuery] bool excludeActiveSessions = false)

@ -144,8 +144,8 @@ public class LibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -218,8 +218,8 @@ public class LibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@ -290,8 +290,8 @@ public class LibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
{
var themeSongs = GetThemeSongs(
itemId,
@ -400,7 +400,7 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
@ -722,10 +722,10 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()

@ -77,7 +77,7 @@ public class LibraryStructureController : BaseJellyfinApiController
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string name,
[FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false)
{

@ -159,10 +159,10 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isDisliked,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
@ -283,8 +283,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
@ -371,8 +371,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
@ -566,7 +566,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId,
[FromQuery] DateTime? minStartDate,
[FromQuery] bool? hasAired,
@ -581,17 +581,17 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isSports,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? seriesTimerId,
[FromQuery] Guid? librarySeriesId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool enableTotalRecordCount = true)
{
userId = RequestHelpers.GetUserId(User, userId);
@ -730,9 +730,9 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isSports,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{

@ -65,7 +65,7 @@ public class MoviesController : BaseJellyfinApiController
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8)
{

@ -76,18 +76,18 @@ public class MusicGenresController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{

@ -67,14 +67,14 @@ public class PersonsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery] Guid? appearsInItemId,
[FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true)

@ -76,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromQuery, ParameterObsolete] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
[FromQuery, ParameterObsolete] Guid? userId,
[FromQuery, ParameterObsolete] MediaType? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
@ -370,7 +370,7 @@ public class PlaylistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
@ -446,7 +446,7 @@ public class PlaylistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> RemoveItemFromPlaylist(
[FromRoute, Required] string playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] entryIds)
{
var callingUserId = User.GetUserId();
@ -493,11 +493,11 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
var callingUserId = userId ?? User.GetUserId();
var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);

@ -84,9 +84,9 @@ public class SearchController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] Guid? userId,
[FromQuery, Required] string searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery] Guid? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,

@ -122,7 +122,7 @@ public class SessionController : BaseJellyfinApiController
public async Task<ActionResult> Play(
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
@ -347,8 +347,8 @@ public class SessionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> PostCapabilities(
[FromQuery] string? id,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsPersistentIdentifier = true)
{

@ -73,13 +73,13 @@ public class StudiosController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,

@ -59,8 +59,8 @@ public class SuggestionsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromQuery] Guid? userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
@ -115,8 +115,8 @@ public class SuggestionsController : BaseJellyfinApiController
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestionsLegacy(
[FromRoute, Required] Guid userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)

@ -212,20 +212,4 @@ public class SystemController : BaseJellyfinApiController
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
return File(stream, "text/plain; charset=utf-8");
}
/// <summary>
/// Gets wake on lan information.
/// </summary>
/// <response code="200">Information retrieved.</response>
/// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
[HttpGet("WakeOnLanInfo")]
[Authorize]
[Obsolete("This endpoint is obsolete.")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
{
var result = _networkManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i));
return Ok(result);
}
}

@ -130,8 +130,8 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@ -149,41 +149,41 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] artists,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] albums,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@ -194,12 +194,12 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{

@ -77,12 +77,12 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] Guid? seriesId,
[FromQuery] Guid? parentId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
@ -143,11 +143,11 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] Guid? parentId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
userId = RequestHelpers.GetUserId(User, userId);
@ -208,7 +208,7 @@ public class TvShowsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
[FromRoute, Required] Guid seriesId,
[FromQuery] Guid? userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] int? season,
[FromQuery] Guid? seasonId,
[FromQuery] bool? isMissing,
@ -218,7 +218,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] ItemSortBy? sortBy)
{
@ -332,13 +332,13 @@ public class TvShowsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
[FromRoute, Required] Guid seriesId,
[FromQuery] Guid? userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing,
[FromQuery] Guid? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
userId = RequestHelpers.GetUserId(User, userId);

@ -98,7 +98,7 @@ public class UniversalAudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,

@ -523,12 +523,12 @@ public class UserLibraryController : BaseJellyfinApiController
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
@ -608,12 +608,12 @@ public class UserLibraryController : BaseJellyfinApiController
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)

@ -66,7 +66,7 @@ public class UserViewsController : BaseJellyfinApiController
public QueryResult<BaseItemDto> GetUserViews(
[FromQuery] Guid? userId,
[FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
{
userId = RequestHelpers.GetUserId(User, userId);
@ -110,7 +110,7 @@ public class UserViewsController : BaseJellyfinApiController
public QueryResult<BaseItemDto> GetUserViewsLegacy(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
=> GetUserViews(userId, includeExternalContent, presetViews, includeHidden);

@ -184,7 +184,7 @@ public class VideosController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
var userId = User.GetUserId();
var items = ids

@ -72,16 +72,16 @@ public class YearsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetYears(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] bool recursive = true,
[FromQuery] bool? enableImages = true)

@ -8,18 +8,18 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.ModelBinders;
/// <summary>
/// Comma delimited array model binder.
/// Comma delimited collection model binder.
/// Returns an empty array of specified type if there is no query parameter.
/// </summary>
public class CommaDelimitedArrayModelBinder : IModelBinder
public class CommaDelimitedCollectionModelBinder : IModelBinder
{
private readonly ILogger<CommaDelimitedArrayModelBinder> _logger;
private readonly ILogger<CommaDelimitedCollectionModelBinder> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
/// Initializes a new instance of the <see cref="CommaDelimitedCollectionModelBinder"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param>
public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger)
/// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedCollectionModelBinder}"/> interface.</param>
public CommaDelimitedCollectionModelBinder(ILogger<CommaDelimitedCollectionModelBinder> logger)
{
_logger = logger;
}

@ -8,18 +8,18 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.ModelBinders;
/// <summary>
/// Comma delimited array model binder.
/// Returns an empty array of specified type if there is no query parameter.
/// Comma delimited collection model binder.
/// Returns an empty collection of specified type if there is no query parameter.
/// </summary>
public class PipeDelimitedArrayModelBinder : IModelBinder
public class PipeDelimitedCollectionModelBinder : IModelBinder
{
private readonly ILogger<PipeDelimitedArrayModelBinder> _logger;
private readonly ILogger<PipeDelimitedCollectionModelBinder> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class.
/// Initializes a new instance of the <see cref="PipeDelimitedCollectionModelBinder"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param>
public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> logger)
/// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedCollectionModelBinder}"/> interface.</param>
public PipeDelimitedCollectionModelBinder(ILogger<PipeDelimitedCollectionModelBinder> logger)
{
_logger = logger;
}

@ -17,7 +17,7 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets the channels to return guide information for.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid>? ChannelIds { get; set; }
/// <summary>
@ -93,25 +93,25 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<ItemSortBy>? SortBy { get; set; }
/// <summary>
/// Gets or sets sort order.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<SortOrder>? SortOrder { get; set; }
/// <summary>
/// Gets or sets the genres to return guide information for.
/// </summary>
[JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonPipeDelimitedCollectionConverterFactory))]
public IReadOnlyList<string>? Genres { get; set; }
/// <summary>
/// Gets or sets the genre ids to return guide information for.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid>? GenreIds { get; set; }
/// <summary>
@ -133,7 +133,7 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets the image types to include in the output.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<ImageType>? EnableImageTypes { get; set; }
/// <summary>
@ -154,6 +154,6 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets specify additional fields of information to return in the output.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<ItemFields>? Fields { get; set; }
}

@ -20,7 +20,7 @@ public class CreatePlaylistDto
/// <summary>
/// Gets or sets item ids to add to the playlist.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid> Ids { get; set; } = [];
/// <summary>

@ -19,7 +19,7 @@ public class UpdatePlaylistDto
/// <summary>
/// Gets or sets item ids of the playlist.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid>? Ids { get; set; }
/// <summary>

@ -70,7 +70,9 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
/// <param name="message">The message.</param>
protected override void Start(WebSocketMessageInfo message)
{
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
if (!message.Connection.AuthorizationInfo.IsApiKey
&& (message.Connection.AuthorizationInfo.User is null
|| !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)))
{
throw new AuthenticationException("Only admin users can retrieve the activity log.");
}

@ -79,7 +79,9 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
/// <param name="message">The message.</param>
protected override void Start(WebSocketMessageInfo message)
{
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
if (!message.Connection.AuthorizationInfo.IsApiKey
&& (message.Connection.AuthorizationInfo.User is null
|| !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)))
{
throw new AuthenticationException("Only admin users can subscribe to session information.");
}

@ -18,11 +18,11 @@ public class BaseItemEntity
public string? Path { get; set; }
public DateTime StartDate { get; set; }
public DateTime? StartDate { get; set; }
public DateTime EndDate { get; set; }
public DateTime? EndDate { get; set; }
public string? ChannelId { get; set; }
public Guid? ChannelId { get; set; }
public bool IsMovie { get; set; }

@ -553,7 +553,7 @@ public sealed class BaseItemRepository
dto.Genres = entity.Genres?.Split('|') ?? [];
dto.DateCreated = entity.DateCreated.GetValueOrDefault();
dto.DateModified = entity.DateModified.GetValueOrDefault();
dto.ChannelId = string.IsNullOrWhiteSpace(entity.ChannelId) ? Guid.Empty : (Guid.TryParse(entity.ChannelId, out var channelId) ? channelId : Guid.Empty);
dto.ChannelId = entity.ChannelId ?? Guid.Empty;
dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
@ -645,7 +645,7 @@ public sealed class BaseItemRepository
// dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
if (dto is IHasStartDate hasStartDate)
{
hasStartDate.StartDate = entity.StartDate;
hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
}
// Fields that are present in the DB but are never actually used
@ -683,12 +683,13 @@ public sealed class BaseItemRepository
entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
entity.Path = GetPathToSave(dto.Path);
entity.EndDate = dto.EndDate.GetValueOrDefault();
entity.EndDate = dto.EndDate;
entity.CommunityRating = dto.CommunityRating;
entity.CustomRating = dto.CustomRating;
entity.IndexNumber = dto.IndexNumber;
entity.IsLocked = dto.IsLocked;
entity.Name = dto.Name;
entity.CleanName = GetCleanValue(dto.Name);
entity.OfficialRating = dto.OfficialRating;
entity.Overview = dto.Overview;
entity.ParentIndexNumber = dto.ParentIndexNumber;
@ -716,7 +717,7 @@ public sealed class BaseItemRepository
entity.Genres = string.Join('|', dto.Genres);
entity.DateCreated = dto.DateCreated;
entity.DateModified = dto.DateModified;
entity.ChannelId = dto.ChannelId.ToString();
entity.ChannelId = dto.ChannelId;
entity.DateLastRefreshed = dto.DateLastRefreshed;
entity.DateLastSaved = dto.DateLastSaved;
entity.OwnerId = dto.OwnerId.ToString();
@ -821,10 +822,9 @@ public sealed class BaseItemRepository
entity.StartDate = hasStartDate.StartDate;
}
entity.UnratedType = dto.GetBlockUnratedType().ToString();
// Fields that are present in the DB but are never actually used
// dto.UnratedType = entity.UnratedType;
// dto.TopParentId = entity.TopParentId;
// dto.CleanName = entity.CleanName;
// dto.UserDataKey = entity.UserDataKey;
if (dto is Folder folder)
@ -854,7 +854,10 @@ public sealed class BaseItemRepository
}
// query = query.DistinctBy(e => e.CleanValue);
return query.Select(e => e.ItemValue.CleanValue).ToArray();
return query.Select(e => e.ItemValue)
.GroupBy(e => e.CleanValue)
.Select(e => e.First().Value)
.ToArray();
}
private static bool TypeRequiresDeserialization(Type type)
@ -1448,8 +1451,7 @@ public sealed class BaseItemRepository
if (filter.ChannelIds.Count > 0)
{
var channelIds = filter.ChannelIds.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray();
baseQuery = baseQuery.Where(e => channelIds.Contains(e.ChannelId));
baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
}
if (!filter.ParentId.IsEmpty())

@ -88,7 +88,7 @@ public class MediaStreamRepository : IMediaStreamRepository
query = query.Where(e => e.StreamType == typeValue);
}
return query;
return query.OrderBy(e => e.StreamIndex);
}
private MediaStream Map(MediaStreamInfo entity)
@ -137,7 +137,7 @@ public class MediaStreamRepository : IMediaStreamRepository
dto.ElPresentFlag = entity.ElPresentFlag;
dto.BlPresentFlag = entity.BlPresentFlag;
dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId;
dto.IsHearingImpaired = entity.IsHearingImpaired;
dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault();
dto.Rotation = entity.Rotation;
if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)

@ -11,6 +11,9 @@ using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Item;
#pragma warning disable RS0030 // Do not use banned APIs
#pragma warning disable CA1304 // Specify CultureInfo
#pragma warning disable CA1311 // Specify a culture or use an invariant version
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
/// <summary>
/// Manager for handling people.
@ -155,7 +158,8 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
if (!string.IsNullOrWhiteSpace(filter.NameContains))
{
query = query.Where(e => e.Name.Contains(filter.NameContains));
var nameContainsUpper = filter.NameContains.ToUpper();
query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper));
}
return query;

@ -4,6 +4,8 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations;
@ -271,4 +273,23 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
// Configuration for each entity is in its own class inside 'ModelConfiguration'.
modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
}
/// <inheritdoc/>
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention());
}
private class DoNotUseReturningClauseConvention : IModelFinalizingConvention
{
public void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
entityType.UseSqlReturningClause(false);
}
}
}
}

@ -0,0 +1 @@
JellyfinDbModelSnapshot.cs binary

@ -0,0 +1,55 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class MakeStartEndDateNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "StartDate",
table: "BaseItems",
type: "TEXT",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "TEXT");
migrationBuilder.AlterColumn<DateTime>(
name: "EndDate",
table: "BaseItems",
type: "TEXT",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "TEXT");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "StartDate",
table: "BaseItems",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "EndDate",
table: "BaseItems",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "TEXT",
oldNullable: true);
}
}
}

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class ChannelIdGuid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// NOOP, Guids and strings are stored the same in SQLite.
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// NOOP, Guids and strings are stored the same in SQLite.
}
}
}

@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@ -152,7 +152,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("Audio")
.HasColumnType("INTEGER");
b.Property<string>("ChannelId")
b.Property<Guid?>("ChannelId")
.HasColumnType("TEXT");
b.Property<string>("CleanName")
@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<DateTime?>("DateModified")
.HasColumnType("TEXT");
b.Property<DateTime>("EndDate")
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT");
b.Property<string>("EpisodeTitle")
@ -323,7 +323,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("SortName")
.HasColumnType("TEXT");
b.Property<DateTime>("StartDate")
b.Property<DateTime?>("StartDate")
.HasColumnType("TEXT");
b.Property<string>("Studios")

@ -116,17 +116,15 @@ namespace Jellyfin.Server.Implementations.Security
DeviceId = deviceId,
Version = version,
Token = token,
IsAuthenticated = false,
HasToken = false
IsAuthenticated = false
};
if (string.IsNullOrWhiteSpace(token))
if (!authInfo.HasToken)
{
// Request doesn't contain a token.
return authInfo;
}
authInfo.HasToken = true;
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{

@ -610,9 +610,11 @@ public class TrickplayManager : ITrickplayManager
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{
var basePath = _config.ApplicationPaths.TrickplayPath;
var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
var path = saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
: Path.Combine(item.GetInternalMetadataPath(), "trickplay");
: Path.Combine(basePath, idString);
var subdirectory = string.Format(
CultureInfo.InvariantCulture,

@ -113,7 +113,7 @@ namespace Jellyfin.Server.Implementations.Users
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
[GeneratedRegex(@"^[\w\ \-'._@+]+$")]
[GeneratedRegex(@"^(?!\s)[\w\ \-'._@+]+(?<!\s)$")]
private static partial Regex ValidUsernameRegex();
/// <inheritdoc/>

@ -89,7 +89,7 @@ public class MigrateLibraryDb : IMigrationRoutine
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName FROM TypedBaseItems
""";
dbContext.BaseItems.ExecuteDelete();
@ -678,7 +678,7 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.EndDate = endDate;
}
if (reader.TryGetString(index++, out var guid))
if (reader.TryGetGuid(index++, out var guid))
{
entity.ChannelId = guid;
}
@ -1034,6 +1034,16 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.MediaType = mediaType;
}
if (reader.TryGetString(index++, out var sortName))
{
entity.SortName = sortName;
}
if (reader.TryGetString(index++, out var cleanName))
{
entity.CleanName = cleanName;
}
var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys);

@ -39,7 +39,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
}
/// <inheritdoc />
public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
/// <inheritdoc />
public string Name => "MoveTrickplayFiles";
@ -89,6 +89,12 @@ public class MoveTrickplayFiles : IMigrationRoutine
{
_fileSystem.MoveDirectory(oldPath, newPath);
}
oldPath = GetNewOldTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false);
if (_fileSystem.DirectoryExists(oldPath))
{
_fileSystem.MoveDirectory(oldPath, newPath);
}
}
} while (previousCount == Limit);
@ -101,4 +107,20 @@ public class MoveTrickplayFiles : IMigrationRoutine
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
}
private string GetNewOldTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{
var path = saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
: Path.Combine(item.GetInternalMetadataPath(), "trickplay");
var subdirectory = string.Format(
CultureInfo.InvariantCulture,
"{0} - {1}x{2}",
width.ToString(CultureInfo.InvariantCulture),
tileWidth.ToString(CultureInfo.InvariantCulture),
tileHeight.ToString(CultureInfo.InvariantCulture));
return Path.Combine(path, subdirectory);
}
}

@ -84,5 +84,11 @@ namespace MediaBrowser.Common.Configuration
/// </summary>
/// <value>The magic string used for virtual path manipulation.</value>
string VirtualDataPath { get; }
/// <summary>
/// Gets the path used for storing trickplay files.
/// </summary>
/// <value>The trickplay path.</value>
string TrickplayPath { get; }
}
}

@ -94,12 +94,6 @@ namespace MediaBrowser.Common.Net
/// <returns>IP address to use, or loopback address if all else fails.</returns>
string GetBindAddress(string source, out int? port);
/// <summary>
/// Get a list of all the MAC addresses associated with active interfaces.
/// </summary>
/// <returns>List of MAC addresses.</returns>
IReadOnlyList<PhysicalAddress> GetMacAddresses();
/// <summary>
/// Returns true if the address is part of the user defined LAN.
/// </summary>

@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Channels
[JsonIgnore]
public override SourceType SourceType => SourceType.Channel;
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
if (blockedChannelsPreference.Length != 0)
@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Channels
}
}
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)

@ -1303,7 +1303,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
if (GetParents().Any(i => !i.IsVisible(user)))
if (GetParents().Any(i => !i.IsVisible(user, true)))
{
return false;
}
@ -1525,13 +1525,14 @@ namespace MediaBrowser.Controller.Entities
/// Determines if a given user has access to this item.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">If user is null.</exception>
public bool IsParentalAllowed(User user)
public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
{
ArgumentNullException.ThrowIfNull(user);
if (!IsVisibleViaTags(user))
if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
{
return false;
}
@ -1603,7 +1604,7 @@ namespace MediaBrowser.Controller.Entities
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
private bool IsVisibleViaTags(User user)
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
{
var allTags = GetInheritedTags();
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
@ -1618,7 +1619,7 @@ namespace MediaBrowser.Controller.Entities
}
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
@ -1658,13 +1659,14 @@ namespace MediaBrowser.Controller.Entities
/// Default is just parental allowed. Can be overridden for more functionality.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
public virtual bool IsVisible(User user)
public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
ArgumentNullException.ThrowIfNull(user);
return IsParentalAllowed(user);
return IsParentalAllowed(user, skipAllowedTagsCheck);
}
public virtual bool IsVisibleStandalone(User user)

@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities
return GetLibraryOptions(Path);
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (GetLibraryOptions().Enabled)
{
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
return false;

@ -219,7 +219,7 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.CreateItem(item, this);
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (this is ICollectionFolder && this is not BasePluginFolder)
{
@ -241,7 +241,7 @@ namespace MediaBrowser.Controller.Entities
}
}
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
/// <summary>
@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.Entities
if (newItems.Count > 0)
{
LibraryManager.CreateOrUpdateItems(newItems, this, cancellationToken);
LibraryManager.CreateItems(newItems, this, cancellationToken);
}
}
else
@ -1202,6 +1202,11 @@ namespace MediaBrowser.Controller.Entities
return false;
}
if (request.Is4K.HasValue)
{
return false;
}
if (request.IsHD.HasValue)
{
return false;

@ -145,14 +145,14 @@ namespace MediaBrowser.Controller.Entities.Movies
return GetItemLookupInfo<BoxSetInfo>();
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (IsLegacyBoxSet)
{
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
if (base.IsVisible(user))
if (base.IsVisible(user, skipAllowedTagsCheck))
{
if (LinkedChildren.Length == 0)
{

@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="items">Items to create.</param>
/// <param name="parent">Parent of new items.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
void CreateOrUpdateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken);
void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken);
/// <summary>
/// Updates the item.

@ -1,6 +1,5 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Net
@ -20,31 +19,31 @@ namespace MediaBrowser.Controller.Net
/// Gets or sets the device identifier.
/// </summary>
/// <value>The device identifier.</value>
public string DeviceId { get; set; }
public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets the device.
/// </summary>
/// <value>The device.</value>
public string Device { get; set; }
public string? Device { get; set; }
/// <summary>
/// Gets or sets the client.
/// </summary>
/// <value>The client.</value>
public string Client { get; set; }
public string? Client { get; set; }
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
public string Version { get; set; }
public string? Version { get; set; }
/// <summary>
/// Gets or sets the token.
/// </summary>
/// <value>The token.</value>
public string Token { get; set; }
public string? Token { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the authorization is from an api key.
@ -54,7 +53,7 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// Gets or sets the user making the request.
/// </summary>
public User User { get; set; }
public User? User { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the token is authenticated.
@ -62,8 +61,9 @@ namespace MediaBrowser.Controller.Net
public bool IsAuthenticated { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the request has a token.
/// Gets a value indicating whether the request has a token.
/// </summary>
public bool HasToken { get; set; }
[MemberNotNullWhen(true, nameof(Token))]
public bool HasToken => !string.IsNullOrWhiteSpace(Token);
}
}

@ -227,11 +227,11 @@ namespace MediaBrowser.Controller.Playlists
return [item];
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (!IsSharedItem)
{
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
if (OpenAccess)

@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
var semaphoreCount = 2 * Environment.ProcessorCount;
// Although the type is not nullable, this might still be null during unit tests
var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
if (semaphoreCount < 1)
{
semaphoreCount = Environment.ProcessorCount;
}
_thumbnailResourcePool = new(semaphoreCount);
}

@ -15,13 +15,13 @@ public class ClientCapabilitiesDto
/// <summary>
/// Gets or sets the list of playable media types.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = [];
/// <summary>
/// Gets or sets the list of supported commands.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = [];
/// <summary>

@ -500,7 +500,7 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets a value indicating whether this instance is for the hearing impaired.
/// </summary>
/// <value><c>true</c> if this instance is for the hearing impaired; otherwise, <c>false</c>.</value>
public bool? IsHearingImpaired { get; set; }
public bool IsHearingImpaired { get; set; }
/// <summary>
/// Gets or sets the height.

@ -84,6 +84,11 @@ namespace MediaBrowser.Model.Entities
/// <summary>
/// The TvMaze provider.
/// </summary>
TvMaze = 19
TvMaze = 19,
/// <summary>
/// The MusicBrainz recording provider.
/// </summary>
MusicBrainzRecording = 20,
}
}

@ -71,6 +71,11 @@ namespace MediaBrowser.Model.Providers
/// <summary>
/// A book.
/// </summary>
Book = 13
Book = 13,
/// <summary>
/// A music recording.
/// </summary>
Recording = 14
}
}

@ -1,47 +0,0 @@
using System.Net.NetworkInformation;
namespace MediaBrowser.Model.System
{
/// <summary>
/// Provides the MAC address and port for wake-on-LAN functionality.
/// </summary>
public class WakeOnLanInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class.
/// </summary>
/// <param name="macAddress">The MAC address.</param>
public WakeOnLanInfo(PhysicalAddress macAddress) : this(macAddress.ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class.
/// </summary>
/// <param name="macAddress">The MAC address.</param>
public WakeOnLanInfo(string macAddress) : this()
{
MacAddress = macAddress;
}
/// <summary>
/// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class.
/// </summary>
public WakeOnLanInfo()
{
Port = 9;
}
/// <summary>
/// Gets the MAC address of the device.
/// </summary>
/// <value>The MAC address.</value>
public string? MacAddress { get; }
/// <summary>
/// Gets or sets the wake-on-LAN port.
/// </summary>
/// <value>The wake-on-LAN port.</value>
public int Port { get; set; }
}
}

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@ -551,10 +552,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var mimetype = response.Content.Headers.ContentType?.MediaType;
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
}
await _providerManager.SaveImage(
item,
stream,
response.Content.Headers.ContentType?.MediaType,
mimetype,
type,
null,
cancellationToken).ConfigureAwait(false);
@ -677,10 +684,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var mimetype = response.Content.Headers.ContentType?.MediaType;
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
}
await _providerManager.SaveImage(
item,
stream,
response.Content.Headers.ContentType?.MediaType,
mimetype,
imageType,
null,
cancellationToken).ConfigureAwait(false);

@ -1162,6 +1162,16 @@ namespace MediaBrowser.Providers.Manager
{
person.ImageUrl = personInSource.ImageUrl;
}
if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
{
person.Role = personInSource.Role;
}
if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
{
person.SortOrder = personInSource.SortOrder;
}
}
}
}

@ -205,27 +205,10 @@ namespace MediaBrowser.Providers.Manager
{
contentType = MediaTypeNames.Image.Png;
}
else
{
// Deduce content type from file extension
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
}
// Throw if we still can't determine the content type
if (string.IsNullOrEmpty(contentType))
{
throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
}
}
// TVDb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
{
throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
}
// some iptv/epg providers don't correctly report media type, extract from url if no extension found
if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType)))
// some providers don't correctly report media type, extract from url if no extension found
if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
// Strip query parameters from url to get actual path.
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
@ -233,7 +216,7 @@ namespace MediaBrowser.Providers.Manager
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
{
throw new HttpRequestException($"Request returned {contentType} instead of an image type", null, HttpStatusCode.NotFound);
throw new HttpRequestException($"Request returned '{contentType}' instead of an image type", null, HttpStatusCode.NotFound);
}
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);

@ -20,6 +20,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using static Jellyfin.Extensions.StringExtensions;
namespace MediaBrowser.Providers.MediaInfo
{
@ -175,11 +176,15 @@ namespace MediaBrowser.Providers.MediaInfo
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
}
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
track.Year ??= mediaInfo.ProductionYear;
track.TrackNumber ??= mediaInfo.IndexNumber;
track.DiscNumber ??= mediaInfo.ParentIndexNumber;
// We should never use the property setter of the ATL.Track class.
// That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
// For example, setting the Year property will also set the Date property, which is not what we want here.
// To properly handle fallback values, we make a clone of those fields when valid.
var trackTitle = (string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title).Trim();
var trackAlbum = (string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album).Trim();
var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
@ -276,22 +281,22 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle))
{
audio.Name = track.Title;
audio.Name = trackTitle;
}
if (options.ReplaceAllMetadata)
{
audio.Album = track.Album.Trim();
audio.IndexNumber = track.TrackNumber;
audio.ParentIndexNumber = track.DiscNumber;
audio.Album = trackAlbum;
audio.IndexNumber = trackTrackNumber;
audio.ParentIndexNumber = trackDiscNumber;
}
else
{
audio.Album ??= track.Album.Trim();
audio.IndexNumber ??= track.TrackNumber;
audio.ParentIndexNumber ??= track.DiscNumber;
audio.Album ??= trackAlbum;
audio.IndexNumber ??= trackTrackNumber;
audio.ParentIndexNumber ??= trackDiscNumber;
}
if (track.Date.HasValue)
@ -299,11 +304,12 @@ namespace MediaBrowser.Providers.MediaInfo
audio.PremiereDate = track.Date;
}
if (track.Year.HasValue)
if (trackYear.HasValue)
{
var year = track.Year.Value;
var year = trackYear.Value;
audio.ProductionYear = year;
// ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks.
if (!audio.PremiereDate.HasValue)
{
try
@ -312,7 +318,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
catch (ArgumentOutOfRangeException ex)
{
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear);
}
}
}
@ -403,6 +409,24 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzRecording, out _))
{
if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_TRACKID", out var recordingMbId)
|| track.AdditionalFields.TryGetValue("MusicBrainz Track Id", out recordingMbId))
&& !string.IsNullOrEmpty(recordingMbId))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, recordingMbId);
}
else if (track.AdditionalFields.TryGetValue("UFID", out var ufIdValue) && !string.IsNullOrEmpty(ufIdValue))
{
// If tagged with MB Picard, the format is 'http://musicbrainz.org\0<recording MBID>'
if (ufIdValue.Contains("musicbrainz.org", StringComparison.OrdinalIgnoreCase))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, ufIdValue.AsSpan().RightPart('\0').ToString());
}
}
}
// Save extracted lyrics if they exist,
// and if the audio doesn't yet have lyrics.
var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics;

@ -119,9 +119,9 @@ namespace MediaBrowser.Providers.MediaInfo
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
{
mediaStream.Index = startIndex++;
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
mediaStream.IsDefault = pathInfo.IsDefault;
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired.GetValueOrDefault();
mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired;
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
}

@ -0,0 +1,27 @@
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
/// <summary>
/// MusicBrainz recording id.
/// </summary>
public class MusicBrainzRecordingId : IExternalId
{
/// <inheritdoc />
public string ProviderName => "MusicBrainz";
/// <inheritdoc />
public string Key => MetadataProvider.MusicBrainzRecording.ToString();
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Recording;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
}

@ -55,13 +55,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
&& !string.IsNullOrEmpty(seriesImdbId)
&& info.IndexNumber.HasValue
&& info.ParentIndexNumber.HasValue)
&& info.IndexNumber.HasValue)
{
result.HasMetadata = await _omdbProvider.FetchEpisodeData(
result,
info.IndexNumber.Value,
info.ParentIndexNumber.Value,
info.ParentIndexNumber ?? 1,
info.GetProviderId(MetadataProvider.Imdb),
seriesImdbId,
info.MetadataLanguage,

@ -63,10 +63,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
var seasonNumber = episode.ParentIndexNumber;
var seasonNumber = episode.ParentIndexNumber ?? 1;
var episodeNumber = episode.IndexNumber;
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
if (!episodeNumber.HasValue)
{
return Enumerable.Empty<RemoteImageInfo>();
}
@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
.ConfigureAwait(false);
var stills = episodeResult?.Images?.Stills;

@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
// The search query must either provide an episode number or date
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
if (!searchInfo.IndexNumber.HasValue)
{
return Enumerable.Empty<RemoteSearchResult>();
}
@ -96,10 +96,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return metadataResult;
}
var seasonNumber = info.ParentIndexNumber;
var seasonNumber = info.ParentIndexNumber ?? 1;
var episodeNumber = info.IndexNumber;
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
if (!episodeNumber.HasValue)
{
return metadataResult;
}
@ -112,7 +112,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
List<TvEpisode>? result = null;
for (int? episode = startindex; episode <= endindex; episode++)
{
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
if (episodeInfo is not null)
{
(result ??= new List<TvEpisode>()).Add(episodeInfo);
@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
else
{
episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.ConfigureAwait(false);
}

@ -68,7 +68,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
if (semaphoreCount < 1)
{
semaphoreCount = 2 * Environment.ProcessorCount;
semaphoreCount = Environment.ProcessorCount;
}
_parallelEncodingLimit = new(semaphoreCount);

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

Loading…
Cancel
Save