mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-06 11:34:57 +02:00
commit
8cc618232b
4 changed files with 279 additions and 262 deletions
|
@ -1,194 +1,41 @@
|
|||
namespace MediaBrowser.Dlna.Channels
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Dlna.ContentDirectory;
|
||||
using MediaBrowser.Dlna.PlayTo;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Dlna.Channels
|
||||
{
|
||||
//public class DlnaChannelFactory : IChannelFactory, IDisposable
|
||||
//public class DlnaChannel : IChannel, IDisposable
|
||||
//{
|
||||
// private readonly IServerConfigurationManager _config;
|
||||
// private readonly ILogger _logger;
|
||||
// private readonly IHttpClient _httpClient;
|
||||
|
||||
// private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
// private readonly SemaphoreSlim _syncLock = new SemaphoreSlim(1, 1);
|
||||
// private readonly IServerConfigurationManager _config;
|
||||
// private List<Device> _servers = new List<Device>();
|
||||
|
||||
// public static DlnaChannelFactory Instance;
|
||||
|
||||
// private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
// private readonly SemaphoreSlim _syncLock = new SemaphoreSlim(1, 1);
|
||||
// private Func<List<string>> _localServersLookup;
|
||||
|
||||
// public DlnaChannelFactory(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IDeviceDiscovery deviceDiscovery)
|
||||
// public static DlnaChannel Current;
|
||||
|
||||
// public DlnaChannel(ILogger logger, IHttpClient httpClient, IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config)
|
||||
// {
|
||||
// _config = config;
|
||||
// _httpClient = httpClient;
|
||||
// _logger = logger;
|
||||
// _httpClient = httpClient;
|
||||
// _deviceDiscovery = deviceDiscovery;
|
||||
// Instance = this;
|
||||
// }
|
||||
|
||||
// internal void Start(Func<List<string>> localServersLookup)
|
||||
// {
|
||||
// _localServersLookup = localServersLookup;
|
||||
|
||||
// //deviceDiscovery.DeviceDiscovered += deviceDiscovery_DeviceDiscovered;
|
||||
// _deviceDiscovery.DeviceLeft += deviceDiscovery_DeviceLeft;
|
||||
// }
|
||||
|
||||
// async void deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
|
||||
// {
|
||||
// string usn;
|
||||
// if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty;
|
||||
|
||||
// string nt;
|
||||
// if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty;
|
||||
|
||||
// string location;
|
||||
// if (!e.Headers.TryGetValue("Location", out location)) location = string.Empty;
|
||||
|
||||
// if (!IsValid(nt, usn))
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (_localServersLookup != null)
|
||||
// {
|
||||
// if (_localServersLookup().Any(i => usn.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
// {
|
||||
// // Don't add the local Dlna server to this
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (GetExistingServers(usn).Any())
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await _syncLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
// try
|
||||
// {
|
||||
// if (GetExistingServers(usn).Any())
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var device = await Device.CreateuPnpDeviceAsync(new Uri(location), _httpClient, _config, _logger)
|
||||
// .ConfigureAwait(false);
|
||||
|
||||
// if (!_servers.Any(i => string.Equals(i.Properties.UUID, device.Properties.UUID, StringComparison.OrdinalIgnoreCase)))
|
||||
// {
|
||||
// _servers.Add(device);
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// _syncLock.Release();
|
||||
// }
|
||||
// }
|
||||
|
||||
// async void deviceDiscovery_DeviceLeft(object sender, SsdpMessageEventArgs e)
|
||||
// {
|
||||
// string usn;
|
||||
// if (!e.Headers.TryGetValue("USN", out usn)) usn = String.Empty;
|
||||
|
||||
// string nt;
|
||||
// if (!e.Headers.TryGetValue("NT", out nt)) nt = String.Empty;
|
||||
|
||||
// if (!IsValid(nt, usn))
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!GetExistingServers(usn).Any())
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await _syncLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var list = _servers.ToList();
|
||||
|
||||
// foreach (var device in GetExistingServers(usn).ToList())
|
||||
// {
|
||||
// list.Remove(device);
|
||||
// }
|
||||
|
||||
// _servers = list;
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// _syncLock.Release();
|
||||
// }
|
||||
// }
|
||||
|
||||
// private bool IsValid(string nt, string usn)
|
||||
// {
|
||||
// // It has to report that it's a media renderer
|
||||
// if (usn.IndexOf("ContentDirectory:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
// nt.IndexOf("ContentDirectory:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
// usn.IndexOf("MediaServer:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
// nt.IndexOf("MediaServer:", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// private IEnumerable<Device> GetExistingServers(string usn)
|
||||
// {
|
||||
// return _servers
|
||||
// .Where(i => usn.IndexOf(i.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1);
|
||||
// }
|
||||
|
||||
// public IEnumerable<IChannel> GetChannels()
|
||||
// {
|
||||
// //if (_servers.Count > 0)
|
||||
// //{
|
||||
// // var service = _servers[0].Properties.Services
|
||||
// // .FirstOrDefault(i => string.Equals(i.ServiceType, "urn:schemas-upnp-org:service:ContentDirectory:1", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// // var controlUrl = service == null ? null : (_servers[0].Properties.BaseUrl.TrimEnd('/') + "/" + service.ControlUrl.TrimStart('/'));
|
||||
|
||||
// // if (!string.IsNullOrEmpty(controlUrl))
|
||||
// // {
|
||||
// // return new List<IChannel>
|
||||
// // {
|
||||
// // new ServerChannel(_servers.ToList(), _httpClient, _logger, controlUrl)
|
||||
// // };
|
||||
// // }
|
||||
// //}
|
||||
|
||||
// return new List<IChannel>();
|
||||
// }
|
||||
|
||||
// public void Dispose()
|
||||
// {
|
||||
// if (_deviceDiscovery != null)
|
||||
// {
|
||||
// _deviceDiscovery.DeviceDiscovered -= deviceDiscovery_DeviceDiscovered;
|
||||
// _deviceDiscovery.DeviceLeft -= deviceDiscovery_DeviceLeft;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
//public class ServerChannel : IChannel, IFactoryChannel
|
||||
//{
|
||||
// private readonly IHttpClient _httpClient;
|
||||
// private readonly ILogger _logger;
|
||||
// public string ControlUrl { get; set; }
|
||||
// public List<Device> Servers { get; set; }
|
||||
|
||||
// public ServerChannel(IHttpClient httpClient, ILogger logger)
|
||||
// {
|
||||
// _httpClient = httpClient;
|
||||
// _logger = logger;
|
||||
// Servers = new List<Device>();
|
||||
// _config = config;
|
||||
// Current = this;
|
||||
// }
|
||||
|
||||
// public string Name
|
||||
|
@ -240,77 +87,9 @@
|
|||
// return true;
|
||||
// }
|
||||
|
||||
// public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
|
||||
// {
|
||||
// IEnumerable<ChannelItemInfo> items;
|
||||
|
||||
// if (string.IsNullOrWhiteSpace(query.FolderId))
|
||||
// {
|
||||
// items = Servers.Select(i => new ChannelItemInfo
|
||||
// {
|
||||
// FolderType = ChannelFolderType.Container,
|
||||
// Id = GetServerId(i),
|
||||
// Name = i.Properties.Name,
|
||||
// Overview = i.Properties.ModelDescription,
|
||||
// Type = ChannelItemType.Folder
|
||||
// });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var idParts = query.FolderId.Split('|');
|
||||
// var folderId = idParts.Length == 2 ? idParts[1] : null;
|
||||
|
||||
// var result = await new ContentDirectoryBrowser(_httpClient, _logger).Browse(new ContentDirectoryBrowseRequest
|
||||
// {
|
||||
// Limit = query.Limit,
|
||||
// StartIndex = query.StartIndex,
|
||||
// ParentId = folderId,
|
||||
// ContentDirectoryUrl = ControlUrl
|
||||
|
||||
// }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// items = result.Items.ToList();
|
||||
// }
|
||||
|
||||
// var list = items.ToList();
|
||||
// var count = list.Count;
|
||||
|
||||
// list = ApplyPaging(list, query).ToList();
|
||||
|
||||
// return new ChannelItemResult
|
||||
// {
|
||||
// Items = list,
|
||||
// TotalRecordCount = count
|
||||
// };
|
||||
// }
|
||||
|
||||
// private string GetServerId(Device device)
|
||||
// {
|
||||
// return device.Properties.UUID.GetMD5().ToString("N");
|
||||
// }
|
||||
|
||||
// private IEnumerable<T> ApplyPaging<T>(IEnumerable<T> items, InternalChannelItemQuery query)
|
||||
// {
|
||||
// if (query.StartIndex.HasValue)
|
||||
// {
|
||||
// items = items.Skip(query.StartIndex.Value);
|
||||
// }
|
||||
|
||||
// if (query.Limit.HasValue)
|
||||
// {
|
||||
// items = items.Take(query.Limit.Value);
|
||||
// }
|
||||
|
||||
// return items;
|
||||
// }
|
||||
|
||||
// public Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken)
|
||||
// {
|
||||
// // TODO: Implement
|
||||
// return Task.FromResult(new DynamicImageResponse
|
||||
// {
|
||||
// HasImage = false
|
||||
// });
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
// public IEnumerable<ImageType> GetSupportedChannelImages()
|
||||
|
@ -320,5 +99,207 @@
|
|||
// ImageType.Primary
|
||||
// };
|
||||
// }
|
||||
|
||||
// public void Start(Func<List<string>> localServersLookup)
|
||||
// {
|
||||
// _localServersLookup = localServersLookup;
|
||||
|
||||
// _deviceDiscovery.DeviceDiscovered -= deviceDiscovery_DeviceDiscovered;
|
||||
// _deviceDiscovery.DeviceLeft -= deviceDiscovery_DeviceLeft;
|
||||
|
||||
// _deviceDiscovery.DeviceDiscovered += deviceDiscovery_DeviceDiscovered;
|
||||
// _deviceDiscovery.DeviceLeft += deviceDiscovery_DeviceLeft;
|
||||
// }
|
||||
|
||||
// public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(query.FolderId))
|
||||
// {
|
||||
// return await GetServers(query, cancellationToken).ConfigureAwait(false);
|
||||
// }
|
||||
|
||||
// return new ChannelItemResult();
|
||||
|
||||
// //var idParts = query.FolderId.Split('|');
|
||||
// //var folderId = idParts.Length == 2 ? idParts[1] : null;
|
||||
|
||||
// //var result = await new ContentDirectoryBrowser(_httpClient, _logger).Browse(new ContentDirectoryBrowseRequest
|
||||
// //{
|
||||
// // Limit = query.Limit,
|
||||
// // StartIndex = query.StartIndex,
|
||||
// // ParentId = folderId,
|
||||
// // ContentDirectoryUrl = ControlUrl
|
||||
|
||||
// //}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// //items = result.Items.ToList();
|
||||
|
||||
// //var list = items.ToList();
|
||||
// //var count = list.Count;
|
||||
|
||||
// //list = ApplyPaging(list, query).ToList();
|
||||
|
||||
// //return new ChannelItemResult
|
||||
// //{
|
||||
// // Items = list,
|
||||
// // TotalRecordCount = count
|
||||
// //};
|
||||
// }
|
||||
|
||||
// public async Task<ChannelItemResult> GetServers(InternalChannelItemQuery query, CancellationToken cancellationToken)
|
||||
// {
|
||||
// await _syncLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var items = _servers.Select(i =>
|
||||
// {
|
||||
// var service = i.Properties.Services
|
||||
// .FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:ContentDirectory:1", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// var controlUrl = service == null ? null : (_servers[0].Properties.BaseUrl.TrimEnd('/') + "/" + service.ControlUrl.TrimStart('/'));
|
||||
|
||||
// if (string.IsNullOrWhiteSpace(controlUrl))
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// return new ChannelItemInfo
|
||||
// {
|
||||
// Id = i.Properties.UUID,
|
||||
// Name = i.Properties.Name,
|
||||
// Type = ChannelItemType.Folder
|
||||
// };
|
||||
|
||||
// }).Where(i => i != null).ToList();
|
||||
|
||||
// return new ChannelItemResult
|
||||
// {
|
||||
// TotalRecordCount = items.Count,
|
||||
// Items = items
|
||||
// };
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// _syncLock.Release();
|
||||
// }
|
||||
// }
|
||||
|
||||
// async void deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
|
||||
// {
|
||||
// string usn;
|
||||
// if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty;
|
||||
|
||||
// string nt;
|
||||
// if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty;
|
||||
|
||||
// string location;
|
||||
// if (!e.Headers.TryGetValue("Location", out location)) location = string.Empty;
|
||||
|
||||
// if (!IsValid(nt, usn))
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (_localServersLookup != null)
|
||||
// {
|
||||
// if (_localServersLookup().Any(i => usn.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
// {
|
||||
// // Don't add the local Dlna server to this
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// await _syncLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
// var serverList = _servers.ToList();
|
||||
|
||||
// try
|
||||
// {
|
||||
// if (GetExistingServers(serverList, usn).Any())
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var device = await Device.CreateuPnpDeviceAsync(new Uri(location), _httpClient, _config, _logger)
|
||||
// .ConfigureAwait(false);
|
||||
|
||||
// if (!serverList.Any(i => string.Equals(i.Properties.UUID, device.Properties.UUID, StringComparison.OrdinalIgnoreCase)))
|
||||
// {
|
||||
// serverList.Add(device);
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// _syncLock.Release();
|
||||
// }
|
||||
// }
|
||||
|
||||
// async void deviceDiscovery_DeviceLeft(object sender, SsdpMessageEventArgs e)
|
||||
// {
|
||||
// string usn;
|
||||
// if (!e.Headers.TryGetValue("USN", out usn)) usn = String.Empty;
|
||||
|
||||
// string nt;
|
||||
// if (!e.Headers.TryGetValue("NT", out nt)) nt = String.Empty;
|
||||
|
||||
// if (!IsValid(nt, usn))
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await _syncLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var serverList = _servers.ToList();
|
||||
|
||||
// var matchingServers = GetExistingServers(serverList, usn);
|
||||
// if (matchingServers.Count > 0)
|
||||
// {
|
||||
// foreach (var device in matchingServers)
|
||||
// {
|
||||
// serverList.Remove(device);
|
||||
// }
|
||||
|
||||
// _servers = serverList;
|
||||
// }
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// _syncLock.Release();
|
||||
// }
|
||||
// }
|
||||
|
||||
// private bool IsValid(string nt, string usn)
|
||||
// {
|
||||
// // It has to report that it's a media renderer
|
||||
// if (usn.IndexOf("ContentDirectory:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
// nt.IndexOf("ContentDirectory:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
// usn.IndexOf("MediaServer:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
// nt.IndexOf("MediaServer:", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// private List<Device> GetExistingServers(List<Device> allDevices, string usn)
|
||||
// {
|
||||
// return allDevices
|
||||
// .Where(i => usn.IndexOf(i.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
// .ToList();
|
||||
// }
|
||||
|
||||
// public void Dispose()
|
||||
// {
|
||||
// _deviceDiscovery.DeviceDiscovered -= deviceDiscovery_DeviceDiscovered;
|
||||
// _deviceDiscovery.DeviceLeft -= deviceDiscovery_DeviceLeft;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ using MediaBrowser.Dlna.Ssdp;
|
|||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Dlna.Channels;
|
||||
|
||||
namespace MediaBrowser.Dlna.Main
|
||||
{
|
||||
|
@ -167,6 +169,8 @@ namespace MediaBrowser.Dlna.Main
|
|||
try
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(_ssdpHandler);
|
||||
|
||||
//DlnaChannel.Current.Start(() => _registeredServerIds.ToList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -548,7 +548,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
private void NormalizeStreamTitle(MediaStream stream)
|
||||
{
|
||||
if (string.Equals(stream.Title, "sdh", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(stream.Title, "sdh", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(stream.Title, "cc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Title = null;
|
||||
}
|
||||
|
|
|
@ -1383,6 +1383,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
}
|
||||
}
|
||||
|
||||
private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, User user)
|
||||
{
|
||||
var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
|
||||
.SelectMany(i => i.Locations)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(i => _libraryManager.FindByPath(i, true))
|
||||
.Where(i => i != null)
|
||||
.Where(i => i.IsVisibleStandalone(user))
|
||||
.ToList();
|
||||
|
||||
var items = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||
{
|
||||
MediaTypes = new[] { MediaType.Video },
|
||||
Recursive = true,
|
||||
AncestorIds = folders.Select(i => i.Id.ToString("N")).ToArray(),
|
||||
ExcludeLocationTypes = new[] { LocationType.Virtual },
|
||||
Limit = Math.Min(10, query.Limit ?? int.MaxValue)
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<QueryResult<BaseItem>> GetInternalRecordings(RecordingQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId);
|
||||
|
@ -1391,6 +1413,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
|
||||
if (user != null && !(query.IsInProgress ?? false))
|
||||
{
|
||||
var initialResult = GetEmbyRecordings(query, user);
|
||||
if (initialResult.TotalRecordCount > 0)
|
||||
{
|
||||
return initialResult;
|
||||
}
|
||||
}
|
||||
|
||||
await RefreshRecordings(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var internalQuery = new InternalItemsQuery(user)
|
||||
|
@ -2060,7 +2091,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var recordings = recordingResult.Items.Cast<ILiveTvRecording>().ToList();
|
||||
var recordings = recordingResult.Items.OfType<ILiveTvRecording>().ToList();
|
||||
|
||||
var groups = new List<BaseItemDto>();
|
||||
|
||||
|
|
Loading…
Reference in a new issue