Merge remote-tracking branch 'jellyfinorigin/master' into feature/DatabaseRefactor

This commit is contained in:
JPVenson 2025-02-19 18:25:00 +00:00
commit d8030147ff
84 changed files with 2561 additions and 688 deletions

View File

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

View File

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

View File

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

View File

@ -34,94 +34,6 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} 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: rename:
name: Rename name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER' if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'

View File

@ -24,31 +24,30 @@
<PackageVersion Include="libse" Version="4.0.10" /> <PackageVersion Include="libse" Version="4.0.10" />
<PackageVersion Include="LrcParser" Version="2024.0728.2" /> <PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.1" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.1" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.1" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.12.0" /> <PackageVersion Include="NEbml" Version="0.12.0" />
@ -76,11 +75,11 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.1" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
<PackageVersion Include="System.Text.Json" Version="9.0.1" /> <PackageVersion Include="System.Text.Json" Version="9.0.2" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.1" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="6.15.0" /> <PackageVersion Include="z440.atl.core" Version="6.16.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" /> <PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />

View File

@ -1812,11 +1812,11 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc /> /// <inheritdoc />
public void CreateItem(BaseItem item, BaseItem? parent) public void CreateItem(BaseItem item, BaseItem? parent)
{ {
CreateOrUpdateItems(new[] { item }, parent, CancellationToken.None); CreateItems(new[] { item }, parent, CancellationToken.None);
} }
/// <inheritdoc /> /// <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); _itemRepository.SaveItems(items, cancellationToken);
@ -2973,11 +2973,11 @@ namespace Emby.Server.Implementations.Library
{ {
if (createEntity) if (createEntity)
{ {
CreateOrUpdateItems([personEntity], null, CancellationToken.None); CreateItems([personEntity], null, CancellationToken.None);
} }
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
CreateOrUpdateItems([personEntity], null, CancellationToken.None); CreateItems([personEntity], null, CancellationToken.None);
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,3 @@
{} {
"Books": "liv"
}

View File

@ -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."
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -184,7 +184,7 @@ public class VideosController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [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 userId = User.GetUserId();
var items = ids var items = ids

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ public class BaseItemEntity
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; } public bool IsMovie { get; set; }

View File

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

View File

@ -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.
}
}
}

View File

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

View File

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

View File

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

View File

@ -11,6 +11,9 @@ using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Item; namespace Jellyfin.Server.Implementations.Item;
#pragma warning disable RS0030 // Do not use banned APIs #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> /// <summary>
/// Manager for handling people. /// Manager for handling people.
@ -155,7 +158,8 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
if (!string.IsNullOrWhiteSpace(filter.NameContains)) 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; return query;

View File

@ -673,7 +673,7 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.EndDate = endDate; entity.EndDate = endDate;
} }
if (reader.TryGetString(index++, out var guid)) if (reader.TryGetGuid(index++, out var guid))
{ {
entity.ChannelId = guid; entity.ChannelId = guid;
} }

View File

@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Channels
[JsonIgnore] [JsonIgnore]
public override SourceType SourceType => SourceType.Channel; 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); var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
if (blockedChannelsPreference.Length != 0) if (blockedChannelsPreference.Length != 0)
@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Channels
} }
} }
return base.IsVisible(user); return base.IsVisible(user, skipAllowedTagsCheck);
} }
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)

View File

@ -1304,7 +1304,7 @@ namespace MediaBrowser.Controller.Entities
return false; return false;
} }
if (GetParents().Any(i => !i.IsVisible(user))) if (GetParents().Any(i => !i.IsVisible(user, true)))
{ {
return false; return false;
} }
@ -1526,13 +1526,14 @@ namespace MediaBrowser.Controller.Entities
/// Determines if a given user has access to this item. /// Determines if a given user has access to this item.
/// </summary> /// </summary>
/// <param name="user">The user.</param> /// <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> /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">If user is null.</exception> /// <exception cref="ArgumentNullException">If user is null.</exception>
public bool IsParentalAllowed(User user) public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
{ {
ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(user);
if (!IsVisibleViaTags(user)) if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
{ {
return false; return false;
} }
@ -1604,7 +1605,7 @@ namespace MediaBrowser.Controller.Entities
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
} }
private bool IsVisibleViaTags(User user) private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
{ {
var allTags = GetInheritedTags(); var allTags = GetInheritedTags();
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase))) if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
@ -1619,7 +1620,7 @@ namespace MediaBrowser.Controller.Entities
} }
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags); 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; return false;
} }
@ -1659,13 +1660,14 @@ namespace MediaBrowser.Controller.Entities
/// Default is just parental allowed. Can be overridden for more functionality. /// Default is just parental allowed. Can be overridden for more functionality.
/// </summary> /// </summary>
/// <param name="user">The user.</param> /// <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> /// <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> /// <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); ArgumentNullException.ThrowIfNull(user);
return IsParentalAllowed(user); return IsParentalAllowed(user, skipAllowedTagsCheck);
} }
public virtual bool IsVisibleStandalone(User user) public virtual bool IsVisibleStandalone(User user)

View File

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

View File

@ -220,7 +220,7 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.CreateItem(item, this); 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) if (this is ICollectionFolder && this is not BasePluginFolder)
{ {
@ -242,7 +242,7 @@ namespace MediaBrowser.Controller.Entities
} }
} }
return base.IsVisible(user); return base.IsVisible(user, skipAllowedTagsCheck);
} }
/// <summary> /// <summary>
@ -453,7 +453,7 @@ namespace MediaBrowser.Controller.Entities
if (newItems.Count > 0) if (newItems.Count > 0)
{ {
LibraryManager.CreateOrUpdateItems(newItems, this, cancellationToken); LibraryManager.CreateItems(newItems, this, cancellationToken);
} }
} }
else else

View File

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

View File

@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="items">Items to create.</param> /// <param name="items">Items to create.</param>
/// <param name="parent">Parent of new items.</param> /// <param name="parent">Parent of new items.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</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> /// <summary>
/// Updates the item. /// Updates the item.

View File

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

View File

@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options); _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter()); _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); _thumbnailResourcePool = new(semaphoreCount);
} }

View File

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

View File

@ -500,7 +500,7 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets a value indicating whether this instance is for the hearing impaired. /// Gets or sets a value indicating whether this instance is for the hearing impaired.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is for the hearing impaired; otherwise, <c>false</c>.</value> /// <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> /// <summary>
/// Gets or sets the height. /// Gets or sets the height.

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -551,10 +552,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.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( await _providerManager.SaveImage(
item, item,
stream, stream,
response.Content.Headers.ContentType?.MediaType, mimetype,
type, type,
null, null,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
@ -677,10 +684,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.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( await _providerManager.SaveImage(
item, item,
stream, stream,
response.Content.Headers.ContentType?.MediaType, mimetype,
imageType, imageType,
null, null,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);

View File

@ -205,27 +205,10 @@ namespace MediaBrowser.Providers.Manager
{ {
contentType = MediaTypeNames.Image.Png; 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... // some providers don't correctly report media type, extract from url if no extension found
if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase)) if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, 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)))
{ {
// Strip query parameters from url to get actual path. // Strip query parameters from url to get actual path.
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path)); contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
@ -233,7 +216,7 @@ namespace MediaBrowser.Providers.Manager
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) 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); var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);

View File

@ -176,9 +176,9 @@ namespace MediaBrowser.Providers.MediaInfo
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
track.Year ??= mediaInfo.ProductionYear; track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
track.TrackNumber ??= mediaInfo.IndexNumber; track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
track.DiscNumber ??= mediaInfo.ParentIndexNumber; track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{ {

View File

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

View File

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

View File

@ -1,15 +1,15 @@
namespace Jellyfin.Extensions.Json.Converters namespace Jellyfin.Extensions.Json.Converters
{ {
/// <summary> /// <summary>
/// Convert comma delimited string to array of type. /// Convert comma delimited string to collection of type.
/// </summary> /// </summary>
/// <typeparam name="T">Type to convert to.</typeparam> /// <typeparam name="T">Type to convert to.</typeparam>
public sealed class JsonCommaDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T> public sealed class JsonCommaDelimitedCollectionConverter<T> : JsonDelimitedCollectionConverter<T>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class. /// Initializes a new instance of the <see cref="JsonCommaDelimitedCollectionConverter{T}"/> class.
/// </summary> /// </summary>
public JsonCommaDelimitedArrayConverter() : base() public JsonCommaDelimitedCollectionConverter() : base()
{ {
} }

View File

@ -1,28 +1,31 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters namespace Jellyfin.Extensions.Json.Converters
{ {
/// <summary> /// <summary>
/// Json comma delimited array converter factory. /// Json comma delimited collection converter factory.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks> /// </remarks>
public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory public class JsonCommaDelimitedCollectionConverterFactory : JsonConverterFactory
{ {
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type typeToConvert) public override bool CanConvert(Type typeToConvert)
{ {
return true; return typeToConvert.IsArray
|| (typeToConvert.IsGenericType
&& (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>))));
} }
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedCollectionConverter<>).MakeGenericType(structType));
} }
} }
} }

View File

@ -10,14 +10,14 @@ namespace Jellyfin.Extensions.Json.Converters
/// Convert delimited string to array of type. /// Convert delimited string to array of type.
/// </summary> /// </summary>
/// <typeparam name="T">Type to convert to.</typeparam> /// <typeparam name="T">Type to convert to.</typeparam>
public abstract class JsonDelimitedArrayConverter<T> : JsonConverter<T[]> public abstract class JsonDelimitedCollectionConverter<T> : JsonConverter<IReadOnlyCollection<T>>
{ {
private readonly TypeConverter _typeConverter; private readonly TypeConverter _typeConverter;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JsonDelimitedArrayConverter{T}"/> class. /// Initializes a new instance of the <see cref="JsonDelimitedCollectionConverter{T}"/> class.
/// </summary> /// </summary>
protected JsonDelimitedArrayConverter() protected JsonDelimitedCollectionConverter()
{ {
_typeConverter = TypeDescriptor.GetConverter(typeof(T)); _typeConverter = TypeDescriptor.GetConverter(typeof(T));
} }
@ -28,7 +28,7 @@ namespace Jellyfin.Extensions.Json.Converters
protected virtual char Delimiter { get; } protected virtual char Delimiter { get; }
/// <inheritdoc /> /// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override IReadOnlyCollection<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType == JsonTokenType.String) if (reader.TokenType == JsonTokenType.String)
{ {
@ -56,35 +56,21 @@ namespace Jellyfin.Extensions.Json.Converters
} }
} }
return typedValues.ToArray(); if (typeToConvert.IsArray)
{
return typedValues.ToArray();
}
return typedValues;
} }
return JsonSerializer.Deserialize<T[]>(ref reader, options); return JsonSerializer.Deserialize<T[]>(ref reader, options);
} }
/// <inheritdoc /> /// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, IReadOnlyCollection<T>? value, JsonSerializerOptions options)
{ {
if (value is not null) JsonSerializer.Serialize(writer, value, options);
{
writer.WriteStartArray();
if (value.Length > 0)
{
foreach (var it in value)
{
if (it is not null)
{
writer.WriteStringValue(it.ToString());
}
}
}
writer.WriteEndArray();
}
else
{
writer.WriteNullValue();
}
} }
} }
} }

View File

@ -4,12 +4,12 @@ namespace Jellyfin.Extensions.Json.Converters
/// Convert Pipe delimited string to array of type. /// Convert Pipe delimited string to array of type.
/// </summary> /// </summary>
/// <typeparam name="T">Type to convert to.</typeparam> /// <typeparam name="T">Type to convert to.</typeparam>
public sealed class JsonPipeDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T> public sealed class JsonPipeDelimitedCollectionConverter<T> : JsonDelimitedCollectionConverter<T>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class. /// Initializes a new instance of the <see cref="JsonPipeDelimitedCollectionConverter{T}"/> class.
/// </summary> /// </summary>
public JsonPipeDelimitedArrayConverter() : base() public JsonPipeDelimitedCollectionConverter() : base()
{ {
} }

View File

@ -1,28 +1,31 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters namespace Jellyfin.Extensions.Json.Converters
{ {
/// <summary> /// <summary>
/// Json Pipe delimited array converter factory. /// Json Pipe delimited collection converter factory.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks> /// </remarks>
public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory public class JsonPipeDelimitedCollectionConverterFactory : JsonConverterFactory
{ {
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type typeToConvert) public override bool CanConvert(Type typeToConvert)
{ {
return true; return typeToConvert.IsArray
|| (typeToConvert.IsGenericType
&& (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>))));
} }
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType)); return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedCollectionConverter<>).MakeGenericType(structType));
} }
} }
} }

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities.Libraries;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.LiveTv.Configuration; using Jellyfin.LiveTv.Configuration;
@ -210,7 +209,7 @@ public class GuideManager : IGuideManager
progress.Report(15); progress.Report(15);
numComplete = 0; numComplete = 0;
var programs = new List<LiveTvProgram>(); var programIds = new List<Guid>();
var channels = new List<Guid>(); var channels = new List<Guid>();
var guideDays = GetGuideDays(); var guideDays = GetGuideDays();
@ -243,8 +242,8 @@ public class GuideManager : IGuideManager
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id); }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
var newPrograms = new List<Guid>(); var newPrograms = new List<LiveTvProgram>();
var updatedPrograms = new List<Guid>(); var updatedPrograms = new List<LiveTvProgram>();
foreach (var program in channelPrograms) foreach (var program in channelPrograms)
{ {
@ -252,14 +251,14 @@ public class GuideManager : IGuideManager
var id = programItem.Id; var id = programItem.Id;
if (isNew) if (isNew)
{ {
newPrograms.Add(id); newPrograms.Add(programItem);
} }
else if (isUpdated) else if (isUpdated)
{ {
updatedPrograms.Add(id); updatedPrograms.Add(programItem);
} }
programs.Add(programItem); programIds.Add(programItem.Id);
isMovie |= program.IsMovie; isMovie |= program.IsMovie;
isSeries |= program.IsSeries; isSeries |= program.IsSeries;
@ -276,21 +275,21 @@ public class GuideManager : IGuideManager
if (newPrograms.Count > 0) if (newPrograms.Count > 0)
{ {
var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList(); _libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken);
_libraryManager.CreateOrUpdateItems(newProgramDtos, null, cancellationToken);
await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
} }
if (updatedPrograms.Count > 0) if (updatedPrograms.Count > 0)
{ {
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
await _libraryManager.UpdateItemsAsync( await _libraryManager.UpdateItemsAsync(
updatedProgramDtos, updatedPrograms,
currentChannel, currentChannel,
ItemUpdateType.MetadataImport, ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
}
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false); await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
}
currentChannel.IsMovie = isMovie; currentChannel.IsMovie = isMovie;
currentChannel.IsNews = isNews; currentChannel.IsNews = isNews;
@ -326,7 +325,6 @@ public class GuideManager : IGuideManager
} }
progress.Report(100); progress.Report(100);
var programIds = programs.Select(p => p.Id).ToList();
return new Tuple<List<Guid>, List<Guid>>(channels, programIds); return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
} }
@ -502,35 +500,27 @@ public class GuideManager : IGuideManager
forceUpdate = true; forceUpdate = true;
} }
var seriesId = info.SeriesId; var channelId = channel.Id;
if (!item.ParentId.Equals(channelId))
if (!item.ParentId.Equals(channel.Id))
{ {
item.ParentId = channel.Id;
forceUpdate = true; forceUpdate = true;
} }
item.ParentId = channel.Id;
item.Audio = info.Audio; item.Audio = info.Audio;
item.ChannelId = channel.Id; item.ChannelId = channelId;
item.CommunityRating ??= info.CommunityRating; item.CommunityRating = info.CommunityRating;
if ((item.CommunityRating ?? 0).Equals(0))
{
item.CommunityRating = null;
}
item.EpisodeTitle = info.EpisodeTitle; item.EpisodeTitle = info.EpisodeTitle;
item.ExternalId = info.Id; item.ExternalId = info.Id;
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal)) var seriesId = info.SeriesId;
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.OrdinalIgnoreCase))
{ {
item.ExternalSeriesId = seriesId;
forceUpdate = true; forceUpdate = true;
} }
item.ExternalSeriesId = seriesId;
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle); var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle)) if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
{ {
item.SeriesName = info.Name; item.SeriesName = info.Name;
@ -578,7 +568,6 @@ public class GuideManager : IGuideManager
} }
item.Tags = tags.ToArray(); item.Tags = tags.ToArray();
item.Genres = info.Genres.ToArray(); item.Genres = info.Genres.ToArray();
if (info.IsHD ?? false) if (info.IsHD ?? false)
@ -589,41 +578,35 @@ public class GuideManager : IGuideManager
item.IsMovie = info.IsMovie; item.IsMovie = info.IsMovie;
item.IsRepeat = info.IsRepeat; item.IsRepeat = info.IsRepeat;
if (item.IsSeries != isSeries) if (item.IsSeries != isSeries)
{ {
item.IsSeries = isSeries;
forceUpdate = true; forceUpdate = true;
} }
item.IsSeries = isSeries;
item.Name = info.Name; item.Name = info.Name;
item.OfficialRating ??= info.OfficialRating; item.OfficialRating = info.OfficialRating;
item.Overview ??= info.Overview; item.Overview = info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
item.ProviderIds = info.ProviderIds;
foreach (var providerId in info.SeriesProviderIds) foreach (var providerId in info.SeriesProviderIds)
{ {
info.ProviderIds["Series" + providerId.Key] = providerId.Value; info.ProviderIds["Series" + providerId.Key] = providerId.Value;
} }
item.ProviderIds = info.ProviderIds;
if (item.StartDate != info.StartDate) if (item.StartDate != info.StartDate)
{ {
item.StartDate = info.StartDate;
forceUpdate = true; forceUpdate = true;
} }
item.StartDate = info.StartDate;
if (item.EndDate != info.EndDate) if (item.EndDate != info.EndDate)
{ {
item.EndDate = info.EndDate;
forceUpdate = true; forceUpdate = true;
} }
item.EndDate = info.EndDate;
item.ProductionYear = info.ProductionYear; item.ProductionYear = info.ProductionYear;
if (!isSeries || info.IsRepeat) if (!isSeries || info.IsRepeat)
{ {
item.PremiereDate = info.OriginalAirDate; item.PremiereDate = info.OriginalAirDate;
@ -632,37 +615,35 @@ public class GuideManager : IGuideManager
item.IndexNumber = info.EpisodeNumber; item.IndexNumber = info.EpisodeNumber;
item.ParentIndexNumber = info.SeasonNumber; item.ParentIndexNumber = info.SeasonNumber;
forceUpdate = forceUpdate || UpdateImages(item, info); forceUpdate |= UpdateImages(item, info);
if (isNew) if (isNew)
{ {
item.OnMetadataChanged(); item.OnMetadataChanged();
return (item, isNew, false); return (item, true, false);
} }
var isUpdated = false; var isUpdated = forceUpdate;
if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) var etag = info.Etag;
if (string.IsNullOrWhiteSpace(etag))
{ {
isUpdated = true; isUpdated = true;
} }
else else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
{ {
var etag = info.Etag; item.SetProviderId(EtagKey, etag);
isUpdated = true;
if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
{
item.SetProviderId(EtagKey, etag);
isUpdated = true;
}
} }
if (isUpdated) if (isUpdated)
{ {
item.OnMetadataChanged(); item.OnMetadataChanged();
return (item, false, true);
} }
return (item, isNew, isUpdated); return (item, false, false);
} }
private static bool UpdateImages(BaseItem item, ProgramInfo info) private static bool UpdateImages(BaseItem item, ProgramInfo info)
@ -679,7 +660,9 @@ public class GuideManager : IGuideManager
updated |= UpdateImage(ImageType.Logo, item, info); updated |= UpdateImage(ImageType.Logo, item, info);
// Backdrop // Backdrop
return updated || UpdateImage(ImageType.Backdrop, item, info); updated |= UpdateImage(ImageType.Backdrop, item, info);
return updated;
} }
private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info) private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
@ -689,7 +672,7 @@ public class GuideManager : IGuideManager
var newImagePath = imageType switch var newImagePath = imageType switch
{ {
ImageType.Primary => info.ImagePath, ImageType.Primary => info.ImagePath,
_ => string.Empty _ => null
}; };
var newImageUrl = imageType switch var newImageUrl = imageType switch
{ {
@ -697,12 +680,12 @@ public class GuideManager : IGuideManager
ImageType.Logo => info.LogoImageUrl, ImageType.Logo => info.LogoImageUrl,
ImageType.Primary => info.ImageUrl, ImageType.Primary => info.ImageUrl,
ImageType.Thumb => info.ThumbImageUrl, ImageType.Thumb => info.ThumbImageUrl,
_ => string.Empty _ => null
}; };
var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false)
|| newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false; || (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false);
if (!differentImage) if (sameImage)
{ {
return false; return false;
} }
@ -757,6 +740,7 @@ public class GuideManager : IGuideManager
var imageInfo = program.ImageInfos[i]; var imageInfo = program.ImageInfos[i];
if (!imageInfo.IsLocalFile) if (!imageInfo.IsLocalFile)
{ {
_logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
try try
{ {
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal( program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(

View File

@ -12,7 +12,7 @@ using Xunit;
namespace Jellyfin.Api.Tests.ModelBinders namespace Jellyfin.Api.Tests.ModelBinders
{ {
public sealed class CommaDelimitedArrayModelBinderTests public sealed class CommaDelimitedCollectionModelBinderTests
{ {
[Fact] [Fact]
public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery() public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery()
@ -22,7 +22,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "lol,xd"; var queryParamString = "lol,xd";
var queryParamType = typeof(string[]); var queryParamType = typeof(string[]);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -47,7 +47,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "42,0"; var queryParamString = "42,0";
var queryParamType = typeof(int[]); var queryParamType = typeof(int[]);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -72,7 +72,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How,Much"; var queryParamString = "How,Much";
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -97,7 +97,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How,,Much"; var queryParamString = "How,,Much";
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -123,7 +123,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "Much"; var queryParamString2 = "Much";
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
@ -151,7 +151,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>(); IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
@ -179,7 +179,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "🔥,😢"; var queryParamString = "🔥,😢";
var queryParamType = typeof(IReadOnlyList<TestType>); var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -205,7 +205,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "😱"; var queryParamString2 = "😱";
var queryParamType = typeof(IReadOnlyList<TestType>); var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>()); var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),

View File

@ -12,7 +12,7 @@ using Xunit;
namespace Jellyfin.Api.Tests.ModelBinders namespace Jellyfin.Api.Tests.ModelBinders
{ {
public sealed class PipeDelimitedArrayModelBinderTests public sealed class PipeDelimitedCollectionModelBinderTests
{ {
[Fact] [Fact]
public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedStringArrayQuery() public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedStringArrayQuery()
@ -22,7 +22,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "lol|xd"; var queryParamString = "lol|xd";
var queryParamType = typeof(string[]); var queryParamType = typeof(string[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -47,7 +47,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "42|0"; var queryParamString = "42|0";
var queryParamType = typeof(int[]); var queryParamType = typeof(int[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -72,7 +72,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How|Much"; var queryParamString = "How|Much";
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -97,7 +97,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How||Much"; var queryParamString = "How||Much";
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -123,7 +123,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "Much"; var queryParamString2 = "Much";
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
@ -151,7 +151,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>(); IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
var queryParamType = typeof(TestType[]); var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
@ -179,7 +179,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "🔥|😢"; var queryParamString = "🔥|😢";
var queryParamType = typeof(IReadOnlyList<TestType>); var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }), new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@ -205,7 +205,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "😱"; var queryParamString2 = "😱";
var queryParamType = typeof(IReadOnlyList<TestType>); var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>()); var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider( var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false), new BindingSource(string.Empty, string.Empty, false, false),

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Jellyfin.Extensions.Tests.Json.Models; using Jellyfin.Extensions.Tests.Json.Models;
@ -7,7 +10,7 @@ using Xunit;
namespace Jellyfin.Extensions.Tests.Json.Converters namespace Jellyfin.Extensions.Tests.Json.Converters
{ {
public class JsonCommaDelimitedArrayTests public class JsonCommaDelimitedCollectionTests
{ {
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
{ {
@ -36,6 +39,29 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
Assert.Equal(desiredValue.Value, value?.Value); Assert.Equal(desiredValue.Value, value?.Value);
} }
[Fact]
public void Deserialize_EmptyList_Success()
{
var desiredValue = new GenericBodyListModel<string>
{
Value = []
};
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": """" }", _jsonOptions));
}
[Fact]
public void Deserialize_EmptyIReadOnlyList_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
{
Value = []
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": """" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact] [Fact]
public void Deserialize_String_Valid_Success() public void Deserialize_String_Valid_Success()
{ {
@ -48,6 +74,17 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
Assert.Equal(desiredValue.Value, value?.Value); Assert.Equal(desiredValue.Value, value?.Value);
} }
[Fact]
public void Deserialize_StringList_Valid_Success()
{
var desiredValue = new GenericBodyListModel<string>
{
Value = ["a", "b", "c"]
};
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions));
}
[Fact] [Fact]
public void Deserialize_String_Space_Valid_Success() public void Deserialize_String_Space_Valid_Success()
{ {
@ -131,5 +168,41 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value); Assert.Equal(desiredValue.Value, value?.Value);
} }
[Fact]
public void Serialize_GenericCommandType_ReadOnlyArray_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyCollectionModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }.AsReadOnly()
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
[Fact]
public void Serialize_GenericCommandType_ImmutableArrayArray_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyCollectionModel<GeneralCommandType>
{
Value = ImmutableArray.Create(new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown })
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
[Fact]
public void Serialize_GenericCommandType_List_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyListModel<GeneralCommandType>
{
Value = new List<GeneralCommandType> { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
} }
} }

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Jellyfin.Extensions.Tests.Json.Models; using Jellyfin.Extensions.Tests.Json.Models;
@ -87,5 +88,17 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value); Assert.Equal(desiredValue.Value, value?.Value);
} }
[Fact]
public void Serialize_GenericCommandType_IReadOnlyList_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyListModel<GeneralCommandType>
{
Value = new List<GeneralCommandType> { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
} }
} }

View File

@ -14,7 +14,7 @@ namespace Jellyfin.Extensions.Tests.Json.Models
/// Gets or sets the value. /// Gets or sets the value.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")]
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public T[] Value { get; set; } = default!; public T[] Value { get; set; } = default!;
} }
} }

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Extensions.Tests.Json.Models
{
/// <summary>
/// The generic body <c>IReadOnlyCollection</c> model.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
public sealed class GenericBodyIReadOnlyCollectionModel<T>
{
/// <summary>
/// Gets or sets the value.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyCollection<T> Value { get; set; } = default!;
}
}

View File

@ -13,7 +13,7 @@ namespace Jellyfin.Extensions.Tests.Json.Models
/// <summary> /// <summary>
/// Gets or sets the value. /// Gets or sets the value.
/// </summary> /// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<T> Value { get; set; } = default!; public IReadOnlyList<T> Value { get; set; } = default!;
} }
} }

View File

@ -0,0 +1,22 @@
#pragma warning disable CA1002 // Do not expose generic lists
#pragma warning disable CA2227 // Collection properties should be read only
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Extensions.Tests.Json.Models
{
/// <summary>
/// The generic body <c>List</c> model.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
public sealed class GenericBodyListModel<T>
{
/// <summary>
/// Gets or sets the value.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public List<T> Value { get; set; } = default!;
}
}

View File

@ -65,7 +65,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.True(res.VideoStream.IsDefault); Assert.True(res.VideoStream.IsDefault);
Assert.False(res.VideoStream.IsExternal); Assert.False(res.VideoStream.IsExternal);
Assert.False(res.VideoStream.IsForced); Assert.False(res.VideoStream.IsForced);
Assert.False(res.VideoStream.IsHearingImpaired.GetValueOrDefault()); Assert.False(res.VideoStream.IsHearingImpaired);
Assert.False(res.VideoStream.IsInterlaced); Assert.False(res.VideoStream.IsInterlaced);
Assert.False(res.VideoStream.IsTextSubtitleStream); Assert.False(res.VideoStream.IsTextSubtitleStream);
Assert.Equal(13d, res.VideoStream.Level); Assert.Equal(13d, res.VideoStream.Level);
@ -152,19 +152,19 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[3].Type); Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[3].Type);
Assert.Equal("DVDSUB", res.MediaStreams[3].Codec); Assert.Equal("DVDSUB", res.MediaStreams[3].Codec);
Assert.Null(res.MediaStreams[3].Title); Assert.Null(res.MediaStreams[3].Title);
Assert.False(res.MediaStreams[3].IsHearingImpaired.GetValueOrDefault()); Assert.False(res.MediaStreams[3].IsHearingImpaired);
Assert.Equal("eng", res.MediaStreams[4].Language); Assert.Equal("eng", res.MediaStreams[4].Language);
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[4].Type); Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[4].Type);
Assert.Equal("mov_text", res.MediaStreams[4].Codec); Assert.Equal("mov_text", res.MediaStreams[4].Codec);
Assert.Null(res.MediaStreams[4].Title); Assert.Null(res.MediaStreams[4].Title);
Assert.True(res.MediaStreams[4].IsHearingImpaired.GetValueOrDefault()); Assert.True(res.MediaStreams[4].IsHearingImpaired);
Assert.Equal("eng", res.MediaStreams[5].Language); Assert.Equal("eng", res.MediaStreams[5].Language);
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[5].Type); Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[5].Type);
Assert.Equal("mov_text", res.MediaStreams[5].Codec); Assert.Equal("mov_text", res.MediaStreams[5].Codec);
Assert.Equal("Commentary", res.MediaStreams[5].Title); Assert.Equal("Commentary", res.MediaStreams[5].Title);
Assert.False(res.MediaStreams[5].IsHearingImpaired.GetValueOrDefault()); Assert.False(res.MediaStreams[5].IsHearingImpaired);
} }
[Fact] [Fact]