Merge pull request #86 from jellyfin/master

Updating from master
This commit is contained in:
BaronGreenback 2020-10-31 12:41:03 +00:00 committed by GitHub
commit c54bdb4a0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 402 additions and 160 deletions

View File

@ -28,6 +28,12 @@ jobs:
inputs: inputs:
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
## Authenticate with npm registry
- task: npmAuthenticate@0
inputs:
workingFile: ./.npmrc
customEndpoint: 'jellyfin-bot for NPM'
## Generate npm api client ## Generate npm api client
# Unstable # Unstable
- task: CmdLine@2 - task: CmdLine@2

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
registry=https://registry.npmjs.org/
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
always-auth=true

View File

@ -1,7 +1,7 @@
{ {
"HeaderLiveTV": "Live-TV", "HeaderLiveTV": "Live-TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
"NameSeasonUnknown": "Tuntematon Kausi", "NameSeasonUnknown": "Tuntematon kausi",
"NameSeasonNumber": "Kausi {0}", "NameSeasonNumber": "Kausi {0}",
"NameInstallFailed": "{0} asennus epäonnistui", "NameInstallFailed": "{0} asennus epäonnistui",
"MusicVideos": "Musiikkivideot", "MusicVideos": "Musiikkivideot",
@ -19,23 +19,23 @@
"ItemAddedWithName": "{0} lisättiin kirjastoon", "ItemAddedWithName": "{0} lisättiin kirjastoon",
"Inherit": "Periytyä", "Inherit": "Periytyä",
"HomeVideos": "Kotivideot", "HomeVideos": "Kotivideot",
"HeaderRecordingGroups": "Nauhoiteryhmät", "HeaderRecordingGroups": "Tallennusryhmät",
"HeaderNextUp": "Seuraavaksi", "HeaderNextUp": "Seuraavaksi",
"HeaderFavoriteSongs": "Lempikappaleet", "HeaderFavoriteSongs": "Suosikkikappaleet",
"HeaderFavoriteShows": "Lempisarjat", "HeaderFavoriteShows": "Suosikkisarjat",
"HeaderFavoriteEpisodes": "Lempijaksot", "HeaderFavoriteEpisodes": "Suosikkijaksot",
"HeaderFavoriteArtists": "Lempiartistit", "HeaderFavoriteArtists": "Suosikkiartistit",
"HeaderFavoriteAlbums": "Lempialbumit", "HeaderFavoriteAlbums": "Suosikkialbumit",
"HeaderContinueWatching": "Jatka katsomista", "HeaderContinueWatching": "Jatka katsomista",
"HeaderAlbumArtists": "Albumin esittäjä", "HeaderAlbumArtists": "Albumin artistit",
"Genres": "Tyylilajit", "Genres": "Tyylilajit",
"Folders": "Kansiot", "Folders": "Kansiot",
"Favorites": "Suosikit", "Favorites": "Suosikit",
"FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}", "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
"DeviceOnlineWithName": "{0} on yhdistetty", "DeviceOnlineWithName": "{0} on yhdistetty",
"DeviceOfflineWithName": "{0} on katkaissut yhteytensä", "DeviceOfflineWithName": "{0} yhteys on katkaistu",
"Collections": "Kokoelmat", "Collections": "Kokoelmat",
"ChapterNameValue": "Luku: {0}", "ChapterNameValue": "Jakso: {0}",
"Channels": "Kanavat", "Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}", "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
"Books": "Kirjat", "Books": "Kirjat",
@ -61,25 +61,25 @@
"UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}", "UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
"UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}", "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
"UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}", "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
"UserOfflineFromDevice": "{0} yhteys katkaistu {1}", "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
"UserLockedOutWithName": "Käyttäjä {0} lukittu", "UserLockedOutWithName": "Käyttäjä {0} lukittu",
"UserDownloadingItemWithValues": "{0} lataa {1}", "UserDownloadingItemWithValues": "{0} lataa {1}",
"UserDeletedWithName": "Käyttäjä {0} poistettu", "UserDeletedWithName": "Käyttäjä {0} poistettu",
"UserCreatedWithName": "Käyttäjä {0} luotu", "UserCreatedWithName": "Käyttäjä {0} luotu",
"TvShows": "TV-sarjat", "TvShows": "TV-ohjelmat",
"Sync": "Synkronoi", "Sync": "Synkronoi",
"SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry", "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
"StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.", "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
"Songs": "Kappaleet", "Songs": "Kappaleet",
"Shows": "Sarjat", "Shows": "Ohjelmat",
"ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen", "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
"ProviderValue": "Tarjoaja: {0}", "ProviderValue": "Tarjoaja: {0}",
"Plugin": "Liitännäinen", "Plugin": "Liitännäinen",
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty", "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
"NotificationOptionVideoPlayback": "Videota toistetaan", "NotificationOptionVideoPlayback": "Videota toistetaan",
"NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui", "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
"NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen", "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
"NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu", "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
"NotificationOptionPluginInstalled": "Liitännäinen asennettu", "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
@ -104,10 +104,10 @@
"TaskRefreshPeople": "Päivitä henkilöt", "TaskRefreshPeople": "Päivitä henkilöt",
"TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.", "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
"TaskCleanLogs": "Puhdista lokihakemisto", "TaskCleanLogs": "Puhdista lokihakemisto",
"TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.", "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
"TaskRefreshLibrary": "Skannaa mediakirjasto", "TaskRefreshLibrary": "Skannaa mediakirjasto",
"TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.", "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
"TaskRefreshChapterImages": "Eristä lukujen kuvat", "TaskRefreshChapterImages": "Pura jakson kuvat",
"TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.", "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto", "TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
"TasksChannelsCategory": "Internet kanavat", "TasksChannelsCategory": "Internet kanavat",

View File

@ -0,0 +1,3 @@
{
"Albums": "आल्बुम्"
}

View File

@ -112,5 +112,5 @@
"Artists": "Artistë", "Artists": "Artistë",
"Application": "Aplikacioni", "Application": "Aplikacioni",
"AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}", "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
"Albums": "Albumet" "Albums": "Albume"
} }

View File

@ -9,7 +9,7 @@
"Channels": "Kanaler", "Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}", "ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar", "Collections": "Samlingar",
"DeviceOfflineWithName": "{0} har kopplat från", "DeviceOfflineWithName": "{0} har kopplat ner",
"DeviceOnlineWithName": "{0} är ansluten", "DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}", "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter", "Favorites": "Favoriter",

View File

@ -101,7 +101,7 @@
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது", "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.", "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.", "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.", "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",

View File

@ -9,6 +9,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,
@ -308,7 +309,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,

View File

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -100,7 +101,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,

View File

@ -109,7 +109,7 @@ namespace Jellyfin.Api.Controllers
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null) if (user.ProfileImage != null)
{ {
_userManager.ClearProfileImage(user); await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
} }
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult DeleteUserImage( public async Task<ActionResult> DeleteUserImage(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null) [FromRoute] int? index = null)
@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
_logger.LogError(e, "Error deleting user profile image:"); _logger.LogError(e, "Error deleting user profile image:");
} }
_userManager.ClearProfileImage(user); await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent(); return NoContent();
} }

View File

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -71,7 +72,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -108,7 +109,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var album = _libraryManager.GetItemById(id); var album = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -145,7 +146,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var playlist = (Playlist)_libraryManager.GetItemById(id); var playlist = (Playlist)_libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -182,7 +183,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
@ -218,7 +219,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -255,7 +256,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -292,7 +293,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)

View File

@ -185,7 +185,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] ItemFilter[] filters, [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] string? imageTypes, [FromQuery] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery] string? genres,
@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,
@ -342,7 +342,7 @@ namespace Jellyfin.Api.Controllers
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(), ImageTypes = imageTypes,
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(), VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
AdjacentTo = adjacentTo, AdjacentTo = adjacentTo,
ItemIds = RequestHelpers.GetGuids(ids), ItemIds = RequestHelpers.GetGuids(ids),
@ -536,7 +536,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,

View File

@ -26,6 +26,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -145,7 +146,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isDisliked, [FromQuery] bool? isDisliked,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields, [FromQuery] string? fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
@ -262,7 +263,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId, [FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields, [FromQuery] string? fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
@ -349,7 +350,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId, [FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields, [FromQuery] string? fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
@ -560,7 +561,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? genreIds, [FromQuery] string? genreIds,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? seriesTimerId, [FromQuery] string? seriesTimerId,
[FromQuery] Guid? librarySeriesId, [FromQuery] Guid? librarySeriesId,
@ -704,7 +705,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSports, [FromQuery] bool? isSports,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? genreIds, [FromQuery] string? genreIds,
[FromQuery] string? fields, [FromQuery] string? fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,

View File

@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,

View File

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,

View File

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists; using MediaBrowser.Model.Playlists;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -151,7 +152,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery] ImageType[] enableImageTypes)
{ {
var playlist = (Playlist)_libraryManager.GetItemById(playlistId); var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null) if (playlist == null)

View File

@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>The list of scheduled tasks.</returns> /// <returns>The list of scheduled tasks.</returns>
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<IScheduledTaskWorker> GetTasks( public IEnumerable<TaskInfo> GetTasks(
[FromQuery] bool? isHidden, [FromQuery] bool? isHidden,
[FromQuery] bool? isEnabled) [FromQuery] bool? isEnabled)
{ {
@ -57,7 +57,7 @@ namespace Jellyfin.Api.Controllers
} }
} }
yield return task; yield return ScheduledTaskHelpers.GetTaskInfo(task);
} }
} }

View File

@ -9,6 +9,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -98,7 +99,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,

View File

@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] ItemFilter[] filters, [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] string? imageTypes, [FromQuery] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery] string? genres,
@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,

View File

@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -77,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] bool? enableImges, [FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
@ -134,7 +135,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] bool? enableImges, [FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData) [FromQuery] bool? enableUserData)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -206,7 +207,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? sortBy) [FromQuery] string? sortBy)
{ {
@ -325,7 +326,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? adjacentTo, [FromQuery] string? adjacentTo,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData) [FromQuery] bool? enableUserData)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)

View File

@ -381,17 +381,13 @@ namespace Jellyfin.Api.Controllers
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
{
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
_userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
}
else
{ {
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
_userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
} }
await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -409,7 +405,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserPolicy( public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy) [FromBody] UserPolicy newPolicy)
{ {
@ -447,7 +443,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.RevokeUserTokens(user.Id, currentToken); _sessionManager.RevokeUserTokens(user.Id, currentToken);
} }
_userManager.UpdatePolicy(userId, newPolicy); await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -464,7 +460,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserConfiguration( public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig) [FromBody] UserConfiguration userConfig)
{ {
@ -473,7 +469,7 @@ namespace Jellyfin.Api.Controllers
return Forbid("User configuration update not allowed"); return Forbid("User configuration update not allowed");
} }
_userManager.UpdateConfiguration(userId, userConfig); await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
return NoContent(); return NoContent();
} }

View File

@ -272,7 +272,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] 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

@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
var baseUrlParam = string.Format( var baseUrlParam = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"\"hls{0}\"", "\"hls/{0}/\"",
Path.GetFileNameWithoutExtension(outputPath)); Path.GetFileNameWithoutExtension(outputPath));
return string.Format( return string.Format(

View File

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -77,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery] 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

@ -126,7 +126,7 @@ namespace Jellyfin.Api.Extensions
bool? enableImages, bool? enableImages,
bool? enableUserData, bool? enableUserData,
int? imageTypeLimit, int? imageTypeLimit,
string? enableImageTypes) ImageType[] enableImageTypes)
{ {
dtoOptions.EnableImages = enableImages ?? true; dtoOptions.EnableImages = enableImages ?? true;
@ -140,11 +140,9 @@ namespace Jellyfin.Api.Extensions
dtoOptions.EnableUserData = enableUserData.Value; dtoOptions.EnableUserData = enableUserData.Value;
} }
if (!string.IsNullOrWhiteSpace(enableImageTypes)) if (enableImageTypes.Length != 0)
{ {
dtoOptions.ImageTypes = enableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) dtoOptions.ImageTypes = enableImageTypes;
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
.ToArray();
} }
return dtoOptions; return dtoOptions;

View File

@ -155,7 +155,7 @@ namespace Jellyfin.Api.Helpers
return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
} }
var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0; var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
var builder = new StringBuilder(); var builder = new StringBuilder();

View File

@ -6,6 +6,7 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -161,5 +162,32 @@ namespace Jellyfin.Api.Helpers
.Select(i => i!.Value) .Select(i => i!.Value)
.ToArray(); .ToArray();
} }
/// <summary>
/// Gets the item fields.
/// </summary>
/// <param name="imageTypes">The image types string.</param>
/// <returns>IEnumerable{ItemFields}.</returns>
internal static ImageType[] GetImageTypes(string? imageTypes)
{
if (string.IsNullOrEmpty(imageTypes))
{
return Array.Empty<ImageType>();
}
return Split(imageTypes, ',', true)
.Select(v =>
{
if (Enum.TryParse(v, true, out ImageType value))
{
return (ImageType?)value;
}
return null;
})
.Where(i => i.HasValue)
.Select(i => i!.Value)
.ToArray();
}
} }
} }

View File

@ -1,4 +1,8 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Api.Models.LiveTvDtos namespace Jellyfin.Api.Models.LiveTvDtos
{ {
@ -137,7 +141,9 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// Gets or sets the image types to include in the output. /// Gets or sets the image types to include in the output.
/// Optional. /// Optional.
/// </summary> /// </summary>
public string? EnableImageTypes { get; set; } [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
[SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "EnableImageTypes", Justification = "Imported from ServiceStack")]
public ImageType[] EnableImageTypes { get; set; } = Array.Empty<ImageType>();
/// <summary> /// <summary>
/// Gets or sets include user data. /// Gets or sets include user data.

View File

@ -2,6 +2,7 @@
#pragma warning disable CA1307 #pragma warning disable CA1307
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -48,6 +49,8 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider; private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IDictionary<Guid, User> _users;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class. /// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary> /// </summary>
@ -81,38 +84,28 @@ namespace Jellyfin.Server.Implementations.Users
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateContext();
foreach (var user in dbContext.Users
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsEnumerable())
{
_users.Add(user.Id, user);
}
} }
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated; public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<User> Users public IEnumerable<User> Users => _users.Values;
{
get
{
using var dbContext = _dbProvider.CreateContext();
return dbContext.Users
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.ToList();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Guid> UsersIds public IEnumerable<Guid> UsersIds => _users.Keys;
{
get
{
using var dbContext = _dbProvider.CreateContext();
return dbContext.Users
.AsQueryable()
.Select(user => user.Id)
.ToList();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public User? GetUserById(Guid id) public User? GetUserById(Guid id)
@ -122,13 +115,8 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
using var dbContext = _dbProvider.CreateContext(); _users.TryGetValue(id, out var user);
return dbContext.Users return user;
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.FirstOrDefault(user => user.Id == id);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -139,14 +127,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name)); throw new ArgumentException("Invalid username", nameof(name));
} }
using var dbContext = _dbProvider.CreateContext(); return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
return dbContext.Users
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsEnumerable()
.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -205,13 +186,17 @@ namespace Jellyfin.Server.Implementations.Users
? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false) ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
: 0; : 0;
return new User( var user = new User(
name, name,
_defaultAuthenticationProvider.GetType().FullName, _defaultAuthenticationProvider.GetType().FullName,
_defaultPasswordResetProvider.GetType().FullName) _defaultPasswordResetProvider.GetType().FullName)
{ {
InternalId = max + 1 InternalId = max + 1
}; };
_users.Add(user.Id, user);
return user;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -237,28 +222,12 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public void DeleteUser(Guid userId) public void DeleteUser(Guid userId)
{ {
using var dbContext = _dbProvider.CreateContext(); if (!_users.TryGetValue(userId, out var user))
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id == userId);
if (user == null)
{ {
throw new ResourceNotFoundException(nameof(userId)); throw new ResourceNotFoundException(nameof(userId));
} }
if (dbContext.Users.Find(user.Id) == null) if (_users.Count == 1)
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
"The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
user.Username,
user.Id));
}
if (dbContext.Users.Count() == 1)
{ {
throw new InvalidOperationException(string.Format( throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -277,6 +246,8 @@ namespace Jellyfin.Server.Implementations.Users
nameof(userId)); nameof(userId));
} }
using var dbContext = _dbProvider.CreateContext();
// Clear all entities related to the user from the database. // Clear all entities related to the user from the database.
if (user.ProfileImage != null) if (user.ProfileImage != null)
{ {
@ -288,6 +259,7 @@ namespace Jellyfin.Server.Implementations.Users
dbContext.RemoveRange(user.AccessSchedules); dbContext.RemoveRange(user.AccessSchedules);
dbContext.Users.Remove(user); dbContext.Users.Remove(user);
dbContext.SaveChanges(); dbContext.SaveChanges();
_users.Remove(userId);
_eventManager.Publish(new UserDeletedEventArgs(user)); _eventManager.Publish(new UserDeletedEventArgs(user));
} }
@ -460,11 +432,9 @@ namespace Jellyfin.Server.Implementations.Users
// the authentication provider might have created it // the authentication provider might have created it
user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user != null)
{ {
UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy()); await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
} }
} }
} }
@ -589,9 +559,7 @@ namespace Jellyfin.Server.Implementations.Users
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist. // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
await using var dbContext = _dbProvider.CreateContext(); if (_users.Any())
if (await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false))
{ {
return; return;
} }
@ -604,6 +572,7 @@ namespace Jellyfin.Server.Implementations.Users
_logger.LogWarning("No users, creating one with username {UserName}", defaultName); _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
await using var dbContext = _dbProvider.CreateContext();
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@ -644,9 +613,9 @@ namespace Jellyfin.Server.Implementations.Users
} }
/// <inheritdoc/> /// <inheritdoc/>
public void UpdateConfiguration(Guid userId, UserConfiguration config) public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
{ {
using var dbContext = _dbProvider.CreateContext(); await using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users var user = dbContext.Users
.Include(u => u.Permissions) .Include(u => u.Permissions)
.Include(u => u.Preferences) .Include(u => u.Preferences)
@ -673,13 +642,13 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user); dbContext.Update(user);
dbContext.SaveChanges(); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void UpdatePolicy(Guid userId, UserPolicy policy) public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
{ {
using var dbContext = _dbProvider.CreateContext(); await using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users var user = dbContext.Users
.Include(u => u.Permissions) .Include(u => u.Permissions)
.Include(u => u.Preferences) .Include(u => u.Preferences)
@ -744,15 +713,16 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user); dbContext.Update(user);
dbContext.SaveChanges(); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void ClearProfileImage(User user) public async Task ClearProfileImageAsync(User user)
{ {
using var dbContext = _dbProvider.CreateContext(); await using var dbContext = _dbProvider.CreateContext();
dbContext.Remove(user.ProfileImage); dbContext.Remove(user.ProfileImage);
dbContext.SaveChanges(); await dbContext.SaveChangesAsync().ConfigureAwait(false);
user.ProfileImage = null;
} }
private static bool IsValidUsername(string name) private static bool IsValidUsername(string name)

View File

@ -42,7 +42,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" />
<PackageReference Include="prometheus-net" Version="3.6.0" /> <PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />

View File

@ -0,0 +1,53 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Convert comma delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
public class JsonCommaDelimitedArrayConverter<T> : JsonConverter<T[]>
{
private readonly TypeConverter _typeConverter;
/// <summary>
/// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class.
/// </summary>
public JsonCommaDelimitedArrayConverter()
{
_typeConverter = TypeDescriptor.GetConverter(typeof(T));
}
/// <inheritdoc />
public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var stringEntries = reader.GetString()?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (stringEntries == null || stringEntries.Length == 0)
{
return Array.Empty<T>();
}
var entries = new T[stringEntries.Length];
for (var i = 0; i < stringEntries.Length; i++)
{
entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i].Trim());
}
return entries;
}
return JsonSerializer.Deserialize<T[]>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Json comma delimited array converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return true;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType();
return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
}
}
}

View File

@ -158,7 +158,8 @@ namespace MediaBrowser.Controller.Library
/// </summary> /// </summary>
/// <param name="userId">The user's Id.</param> /// <param name="userId">The user's Id.</param>
/// <param name="config">The request containing the new user configuration.</param> /// <param name="config">The request containing the new user configuration.</param>
void UpdateConfiguration(Guid userId, UserConfiguration config); /// <returns>A task representing the update.</returns>
Task UpdateConfigurationAsync(Guid userId, UserConfiguration config);
/// <summary> /// <summary>
/// This method updates the user's policy. /// This method updates the user's policy.
@ -167,12 +168,14 @@ namespace MediaBrowser.Controller.Library
/// </summary> /// </summary>
/// <param name="userId">The user's Id.</param> /// <param name="userId">The user's Id.</param>
/// <param name="policy">The request containing the new user policy.</param> /// <param name="policy">The request containing the new user policy.</param>
void UpdatePolicy(Guid userId, UserPolicy policy); /// <returns>A task representing the update.</returns>
Task UpdatePolicyAsync(Guid userId, UserPolicy policy);
/// <summary> /// <summary>
/// Clears the user's profile image. /// Clears the user's profile image.
/// </summary> /// </summary>
/// <param name="user">The user.</param> /// <param name="user">The user.</param>
void ClearProfileImage(User user); /// <returns>A task representing the clearing of the profile image.</returns>
Task ClearProfileImageAsync(User user);
} }
} }

View File

@ -1380,24 +1380,40 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
{ {
if (audioStream == null)
{
return null;
}
if (request.AudioBitRate.HasValue) if (request.AudioBitRate.HasValue)
{ {
// Don't encode any higher than this // Don't encode any higher than this
return Math.Min(384000, request.AudioBitRate.Value); return Math.Min(384000, request.AudioBitRate.Value);
} }
return null; // Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
return 128000;
} }
public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream) public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
{ {
if (audioStream == null)
{
return null;
}
if (audioBitRate.HasValue) if (audioBitRate.HasValue)
{ {
// Don't encode any higher than this // Don't encode any higher than this
return Math.Min(384000, audioBitRate.Value); return Math.Min(384000, audioBitRate.Value);
} }
return null; // Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
return 128000;
} }
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls) public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)

View File

@ -666,6 +666,16 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
// Interlaced video streams in Matroska containers return the field rate instead of the frame rate
// as both the average and real frame rate, so we half the returned frame rates to get the correct values
//
// https://gitlab.com/mbunkus/mkvtoolnix/-/wikis/Wrong-frame-rate-displayed
if (stream.IsInterlaced && formatInfo.FormatName.Contains("matroska", StringComparison.OrdinalIgnoreCase))
{
stream.AverageFrameRate /= 2;
stream.RealFrameRate /= 2;
}
if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
{ {

View File

@ -22,7 +22,7 @@
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" /> <PackageReference Include="coverlet.collector" Version="1.3.0" />
<PackageReference Include="Moq" Version="4.14.6" /> <PackageReference Include="Moq" Version="4.14.7" />
</ItemGroup> </ItemGroup>
<!-- Code Analyzers --> <!-- Code Analyzers -->

View File

@ -0,0 +1,92 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Common.Tests.Models;
using MediaBrowser.Model.Session;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
public static class JsonCommaDelimitedArrayTests
{
[Fact]
public static void Deserialize_String_Valid_Success()
{
var desiredValue = new GenericBodyModel<string>
{
Value = new[] { "a", "b", "c" }
};
var options = new JsonSerializerOptions();
var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_String_Space_Valid_Success()
{
var desiredValue = new GenericBodyModel<string>
{
Value = new[] { "a", "b", "c" }
};
var options = new JsonSerializerOptions();
var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Valid_Success()
{
var desiredValue = new GenericBodyModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Space_Valid_Success()
{
var desiredValue = new GenericBodyModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_String_Array_Valid_Success()
{
var desiredValue = new GenericBodyModel<string>
{
Value = new[] { "a", "b", "c" }
};
var options = new JsonSerializerOptions();
var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Array_Valid_Success()
{
var desiredValue = new GenericBodyModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
}
}

View File

@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using MediaBrowser.Common.Json.Converters;
namespace Jellyfin.Common.Tests.Models
{
/// <summary>
/// The generic body model.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
public class GenericBodyModel<T>
{
/// <summary>
/// Gets or sets the value.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")]
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public T[] Value { get; set; } = default!;
}
}

View File

@ -17,7 +17,7 @@
<PackageReference Include="AutoFixture" Version="4.13.0" /> <PackageReference Include="AutoFixture" Version="4.13.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Moq" Version="4.14.6" /> <PackageReference Include="Moq" Version="4.14.7" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" /> <PackageReference Include="coverlet.collector" Version="1.3.0" />