diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
new file mode 100644
index 0000000000..4c587391fc
--- /dev/null
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Extensions
+{
+ ///
+ /// Dto Extensions.
+ ///
+ public static class DtoExtensions
+ {
+ ///
+ /// Add Dto Item fields.
+ ///
+ ///
+ /// Converted from IHasItemFields.
+ /// Legacy order: 1.
+ ///
+ /// DtoOptions object.
+ /// Comma delimited string of fields.
+ /// Modified DtoOptions object.
+ internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields)
+ {
+ if (string.IsNullOrEmpty(fields))
+ {
+ dtoOptions.Fields = Array.Empty();
+ }
+ else
+ {
+ dtoOptions.Fields = fields.Split(',')
+ .Select(v =>
+ {
+ if (Enum.TryParse(v, true, out ItemFields value))
+ {
+ return (ItemFields?)value;
+ }
+
+ return null;
+ })
+ .Where(i => i.HasValue)
+ .Select(i => i!.Value)
+ .ToArray();
+ }
+
+ return dtoOptions;
+ }
+
+ ///
+ /// Add additional fields depending on client.
+ ///
+ ///
+ /// Use in place of GetDtoOptions.
+ /// Legacy order: 2.
+ ///
+ /// DtoOptions object.
+ /// Current request.
+ /// Modified DtoOptions object.
+ internal static DtoOptions AddClientFields(
+ this DtoOptions dtoOptions, HttpRequest request)
+ {
+ dtoOptions.Fields ??= Array.Empty();
+
+ string? client = ClaimHelpers.GetClient(request.HttpContext.User);
+
+ // No client in claim
+ if (string.IsNullOrEmpty(client))
+ {
+ return dtoOptions;
+ }
+
+ if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
+ {
+ if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ int oldLen = dtoOptions.Fields.Length;
+ var arr = new ItemFields[oldLen + 1];
+ dtoOptions.Fields.CopyTo(arr, 0);
+ arr[oldLen] = ItemFields.RecursiveItemCount;
+ dtoOptions.Fields = arr;
+ }
+ }
+
+ if (!dtoOptions.ContainsField(ItemFields.ChildCount))
+ {
+ if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
+ client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ int oldLen = dtoOptions.Fields.Length;
+ var arr = new ItemFields[oldLen + 1];
+ dtoOptions.Fields.CopyTo(arr, 0);
+ arr[oldLen] = ItemFields.ChildCount;
+ dtoOptions.Fields = arr;
+ }
+ }
+
+ return dtoOptions;
+ }
+
+ ///
+ /// Add additional DtoOptions.
+ ///
+ ///
+ /// Converted from IHasDtoOptions.
+ /// Legacy order: 3.
+ ///
+ /// DtoOptions object.
+ /// Enable images.
+ /// Enable user data.
+ /// Image type limit.
+ /// Enable image types.
+ /// Modified DtoOptions object.
+ internal static DtoOptions AddAdditionalDtoOptions(
+ in DtoOptions dtoOptions,
+ bool? enableImages,
+ bool? enableUserData,
+ int? imageTypeLimit,
+ string enableImageTypes)
+ {
+ dtoOptions.EnableImages = enableImages ?? true;
+
+ if (imageTypeLimit.HasValue)
+ {
+ dtoOptions.ImageTypeLimit = imageTypeLimit.Value;
+ }
+
+ if (enableUserData.HasValue)
+ {
+ dtoOptions.EnableUserData = enableUserData.Value;
+ }
+
+ if (!string.IsNullOrWhiteSpace(enableImageTypes))
+ {
+ dtoOptions.ImageTypes = enableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
+ .ToArray();
+ }
+
+ return dtoOptions;
+ }
+
+ ///
+ /// Check if DtoOptions contains field.
+ ///
+ /// DtoOptions object.
+ /// Field to check.
+ /// Field existence.
+ internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field)
+ => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field);
+ }
+}