New: Bulk Manage Applications, Download Clients

Co-authored-by: Qstick <qstick@gmail.com>
This commit is contained in:
Bogdan
2023-07-09 20:43:36 +03:00
parent cb520b2264
commit 1706728230
85 changed files with 2366 additions and 255 deletions

View File

@@ -48,6 +48,8 @@
"ApplyTagsHelpTexts3": "Remove: Remove the entered tags",
"ApplyTagsHelpTexts4": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)",
"Apps": "Apps",
"AppsMinimumSeeders": "Apps Minimum Seeders",
"AppsMinimumSeedersHelpText": "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default",
"AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?",
"AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?",
"Artist": "Artist",
@@ -102,7 +104,9 @@
"ConnectionLostMessage": "Prowlarr has lost its connection to the backend and will need to be reloaded to restore functionality.",
"Connections": "Connections",
"CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update",
"CountIndexersSelected": "{0} indexers selected",
"CountApplicationsSelected": "{0} application(s) selected",
"CountDownloadClientsSelected": "{0} download client(s) selected",
"CountIndexersSelected": "{0} indexer(s) selected",
"Custom": "Custom",
"CustomFilters": "Custom Filters",
"DBMigration": "DB Migration",
@@ -122,6 +126,10 @@
"DeleteIndexerProxyMessageText": "Are you sure you want to delete the proxy '{0}'?",
"DeleteNotification": "Delete Notification",
"DeleteNotificationMessageText": "Are you sure you want to delete the notification '{0}'?",
"DeleteSelectedApplications": "Delete Selected Applications",
"DeleteSelectedApplicationsMessageText": "Are you sure you want to delete {0} selected application(s)?",
"DeleteSelectedDownloadClients": "Delete Download Client(s)",
"DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {0} selected download client(s)?",
"DeleteSelectedIndexer": "Delete Selected Indexer",
"DeleteSelectedIndexers": "Delete Selected Indexers",
"DeleteSelectedIndexersMessageText": "Are you sure you want to delete {0} selected indexer(s)?",
@@ -137,6 +145,7 @@
"Donations": "Donations",
"DownloadClient": "Download Client",
"DownloadClientCategory": "Download Client Category",
"DownloadClientPriorityHelpText": "Prioritize multiple Download Clients. Round-Robin is used for clients with the same priority.",
"DownloadClientSettings": "Download Client Settings",
"DownloadClientStatusCheckAllClientMessage": "All download clients are unavailable due to failures",
"DownloadClientStatusCheckSingleClientMessage": "Download clients unavailable due to failures: {0}",
@@ -145,6 +154,7 @@
"Duration": "Duration",
"Edit": "Edit",
"EditIndexer": "Edit Indexer",
"EditSelectedDownloadClients": "Edit Selected Download Clients",
"EditSelectedIndexers": "Edit Selected Indexers",
"EditSyncProfile": "Edit Sync Profile",
"ElapsedTime": "Elapsed Time",
@@ -204,6 +214,7 @@
"Id": "Id",
"IgnoredAddresses": "Ignored Addresses",
"IllRestartLater": "I'll restart later",
"Implementation": "Implementation",
"IncludeHealthWarningsHelpText": "Include Health Warnings",
"IncludeManualGrabsHelpText": "Include Manual Grabs made within Prowlarr",
"Indexer": "Indexer",
@@ -263,6 +274,8 @@
"Logs": "Logs",
"MIA": "MIA",
"MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details",
"ManageApplications": "Manage Applications",
"ManageDownloadClients": "Manage Download Clients",
"Manual": "Manual",
"MappedCategories": "Mapped Categories",
"MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information",
@@ -287,6 +300,7 @@
"NoBackupsAreAvailable": "No backups are available",
"NoChange": "No Change",
"NoChanges": "No Changes",
"NoDownloadClientsFound": "No download clients found",
"NoHistoryFound": "No history found",
"NoIndexersFound": "No indexers found",
"NoLeaveIt": "No, Leave It",
@@ -313,6 +327,8 @@
"OpenBrowserOnStart": "Open browser on start",
"OpenThisModal": "Open This Modal",
"Options": "Options",
"PackSeedTime": "Pack Seed Time",
"PackSeedTimeHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default",
"PackageVersion": "Package Version",
"PageSize": "Page Size",
"PageSizeHelpText": "Number of items to show on each page",
@@ -326,8 +342,6 @@
"PortNumber": "Port Number",
"Presets": "Presets",
"Priority": "Priority",
"PriorityHelpText": "Prioritize multiple Download Clients. Round-Robin is used for clients with the same priority.",
"PrioritySettings": "Priority",
"Privacy": "Privacy",
"Private": "Private",
"Protocol": "Protocol",
@@ -398,6 +412,10 @@
"SearchTypes": "Search Types",
"Season": "Season",
"Security": "Security",
"SeedRatio": "Seed Ratio",
"SeedRatioHelpText": "The ratio a torrent should reach before stopping, empty is app's default",
"SeedTime": "Seed Time",
"SeedTimeHelpText": "The time a torrent should be seeded before stopping, empty is app's default",
"Seeders": "Seeders",
"SelectAll": "Select All",
"SelectIndexers": "Select Indexers",

View File

@@ -12,9 +12,10 @@ namespace NzbDrone.Core.ThingiProvider
bool Exists(int id);
TProviderDefinition Find(int id);
TProviderDefinition Get(int id);
IEnumerable<TProviderDefinition> Get(IEnumerable<int> ids);
TProviderDefinition Create(TProviderDefinition definition);
void Update(TProviderDefinition definition);
void Update(IEnumerable<TProviderDefinition> definitions);
IEnumerable<TProviderDefinition> Update(IEnumerable<TProviderDefinition> definitions);
void Delete(int id);
void Delete(IEnumerable<int> ids);
IEnumerable<TProviderDefinition> GetDefaultDefinitions();

View File

@@ -101,6 +101,11 @@ namespace NzbDrone.Core.ThingiProvider
return _providerRepository.Get(id);
}
public IEnumerable<TProviderDefinition> Get(IEnumerable<int> ids)
{
return _providerRepository.Get(ids);
}
public TProviderDefinition Find(int id)
{
return _providerRepository.Find(id);
@@ -120,10 +125,12 @@ namespace NzbDrone.Core.ThingiProvider
_eventAggregator.PublishEvent(new ProviderUpdatedEvent<TProvider>(updatedDef));
}
public virtual void Update(IEnumerable<TProviderDefinition> definitions)
public virtual IEnumerable<TProviderDefinition> Update(IEnumerable<TProviderDefinition> definitions)
{
_providerRepository.UpdateMany(definitions.ToList());
_eventAggregator.PublishEvent(new ProviderBulkUpdatedEvent<TProvider>(definitions));
return definitions;
}
public void Delete(int id)

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using NzbDrone.Core.Applications;
namespace Prowlarr.Api.V1.Applications
{
public class ApplicationBulkResource : ProviderBulkResource<ApplicationBulkResource>
{
public ApplicationSyncLevel? SyncLevel { get; set; }
}
public class ApplicationBulkResourceMapper : ProviderBulkResourceMapper<ApplicationBulkResource, ApplicationDefinition>
{
public override List<ApplicationDefinition> UpdateModel(ApplicationBulkResource resource, List<ApplicationDefinition> existingDefinitions)
{
if (resource == null)
{
return new List<ApplicationDefinition>();
}
existingDefinitions.ForEach(existing =>
{
existing.SyncLevel = resource.SyncLevel ?? existing.SyncLevel;
});
return existingDefinitions;
}
}
}

View File

@@ -1,15 +1,16 @@
using NzbDrone.Core.Applications;
using Prowlarr.Http;
namespace Prowlarr.Api.V1.Application
namespace Prowlarr.Api.V1.Applications
{
[V1ApiController("applications")]
public class ApplicationController : ProviderControllerBase<ApplicationResource, IApplication, ApplicationDefinition>
public class ApplicationController : ProviderControllerBase<ApplicationResource, ApplicationBulkResource, IApplication, ApplicationDefinition>
{
public static readonly ApplicationResourceMapper ResourceMapper = new ApplicationResourceMapper();
public static readonly ApplicationResourceMapper ResourceMapper = new ();
public static readonly ApplicationBulkResourceMapper BulkResourceMapper = new ();
public ApplicationController(ApplicationFactory applicationsFactory)
: base(applicationsFactory, "applications", ResourceMapper)
: base(applicationsFactory, "applications", ResourceMapper, BulkResourceMapper)
{
}
}

View File

@@ -1,6 +1,6 @@
using NzbDrone.Core.Applications;
namespace Prowlarr.Api.V1.Application
namespace Prowlarr.Api.V1.Applications
{
public class ApplicationResource : ProviderResource<ApplicationResource>
{

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using NzbDrone.Core.Download;
namespace Prowlarr.Api.V1.DownloadClient
{
public class DownloadClientBulkResource : ProviderBulkResource<DownloadClientBulkResource>
{
public bool? Enable { get; set; }
public int? Priority { get; set; }
}
public class DownloadClientBulkResourceMapper : ProviderBulkResourceMapper<DownloadClientBulkResource, DownloadClientDefinition>
{
public override List<DownloadClientDefinition> UpdateModel(DownloadClientBulkResource resource, List<DownloadClientDefinition> existingDefinitions)
{
if (resource == null)
{
return new List<DownloadClientDefinition>();
}
existingDefinitions.ForEach(existing =>
{
existing.Enable = resource.Enable ?? existing.Enable;
existing.Priority = resource.Priority ?? existing.Priority;
});
return existingDefinitions;
}
}
}

View File

@@ -4,12 +4,13 @@ using Prowlarr.Http;
namespace Prowlarr.Api.V1.DownloadClient
{
[V1ApiController]
public class DownloadClientController : ProviderControllerBase<DownloadClientResource, IDownloadClient, DownloadClientDefinition>
public class DownloadClientController : ProviderControllerBase<DownloadClientResource, DownloadClientBulkResource, IDownloadClient, DownloadClientDefinition>
{
public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper();
public static readonly DownloadClientResourceMapper ResourceMapper = new ();
public static readonly DownloadClientBulkResourceMapper BulkResourceMapper = new ();
public DownloadClientController(IDownloadClientFactory downloadClientFactory)
: base(downloadClientFactory, "downloadclient", ResourceMapper)
: base(downloadClientFactory, "downloadclient", ResourceMapper, BulkResourceMapper)
{
}
}

View File

@@ -0,0 +1,12 @@
using NzbDrone.Core.IndexerProxies;
namespace Prowlarr.Api.V1.IndexerProxies
{
public class IndexerProxyBulkResource : ProviderBulkResource<IndexerProxyBulkResource>
{
}
public class IndexerProxyBulkResourceMapper : ProviderBulkResourceMapper<IndexerProxyBulkResource, IndexerProxyDefinition>
{
}
}

View File

@@ -1,16 +1,31 @@
using System;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.IndexerProxies;
using Prowlarr.Http;
namespace Prowlarr.Api.V1.IndexerProxies
{
[V1ApiController]
public class IndexerProxyController : ProviderControllerBase<IndexerProxyResource, IIndexerProxy, IndexerProxyDefinition>
public class IndexerProxyController : ProviderControllerBase<IndexerProxyResource, IndexerProxyBulkResource, IIndexerProxy, IndexerProxyDefinition>
{
public static readonly IndexerProxyResourceMapper ResourceMapper = new IndexerProxyResourceMapper();
public static readonly IndexerProxyResourceMapper ResourceMapper = new ();
public static readonly IndexerProxyBulkResourceMapper BulkResourceMapper = new ();
public IndexerProxyController(IndexerProxyFactory notificationFactory)
: base(notificationFactory, "indexerProxy", ResourceMapper)
: base(notificationFactory, "indexerProxy", ResourceMapper, BulkResourceMapper)
{
}
[NonAction]
public override ActionResult<IndexerProxyResource> UpdateProvider([FromBody] IndexerProxyBulkResource providerResource)
{
throw new NotImplementedException();
}
[NonAction]
public override object DeleteProviders([FromBody] IndexerProxyBulkResource resource)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using NzbDrone.Core.Indexers;
namespace Prowlarr.Api.V1.Indexers
{
public class IndexerBulkResource : ProviderBulkResource<IndexerBulkResource>
{
public bool? Enable { get; set; }
public int? AppProfileId { get; set; }
public int? Priority { get; set; }
public int? MinimumSeeders { get; set; }
public double? SeedRatio { get; set; }
public int? SeedTime { get; set; }
public int? PackSeedTime { get; set; }
}
public class IndexerBulkResourceMapper : ProviderBulkResourceMapper<IndexerBulkResource, IndexerDefinition>
{
public override List<IndexerDefinition> UpdateModel(IndexerBulkResource resource, List<IndexerDefinition> existingDefinitions)
{
if (resource == null)
{
return new List<IndexerDefinition>();
}
existingDefinitions.ForEach(existing =>
{
existing.Enable = resource.Enable ?? existing.Enable;
existing.AppProfileId = resource.AppProfileId ?? existing.AppProfileId;
existing.Priority = resource.Priority ?? existing.Priority;
if (existing.Protocol == DownloadProtocol.Torrent)
{
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.AppMinimumSeeders = resource.MinimumSeeders ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.AppMinimumSeeders;
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio = resource.SeedRatio ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio;
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime = resource.SeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime;
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime = resource.PackSeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime;
}
});
return existingDefinitions;
}
}
}

View File

@@ -4,10 +4,10 @@ using Prowlarr.Http;
namespace Prowlarr.Api.V1.Indexers
{
[V1ApiController]
public class IndexerController : ProviderControllerBase<IndexerResource, IIndexer, IndexerDefinition>
public class IndexerController : ProviderControllerBase<IndexerResource, IndexerBulkResource, IIndexer, IndexerDefinition>
{
public IndexerController(IndexerFactory indexerFactory, IndexerResourceMapper resourceMapper)
: base(indexerFactory, "indexer", resourceMapper)
public IndexerController(IndexerFactory indexerFactory, IndexerResourceMapper resourceMapper, IndexerBulkResourceMapper bulkResourceMapper)
: base(indexerFactory, "indexer", resourceMapper, bulkResourceMapper)
{
}
}

View File

@@ -1,88 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Commands;
using Prowlarr.Http;
namespace Prowlarr.Api.V1.Indexers
{
[V1ApiController("indexer/editor")]
public class IndexerEditorController : Controller
{
private readonly IIndexerFactory _indexerFactory;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IndexerResourceMapper _resourceMapper;
public IndexerEditorController(IIndexerFactory indexerFactory, IManageCommandQueue commandQueueManager, IndexerResourceMapper resourceMapper)
{
_indexerFactory = indexerFactory;
_commandQueueManager = commandQueueManager;
_resourceMapper = resourceMapper;
}
[HttpPut]
[Consumes("application/json")]
public IActionResult SaveAll(IndexerEditorResource resource)
{
var indexersToUpdate = _indexerFactory.AllProviders(false).Select(x => (IndexerDefinition)x.Definition).Where(d => resource.IndexerIds.Contains(d.Id));
foreach (var indexer in indexersToUpdate)
{
if (resource.Enable.HasValue)
{
indexer.Enable = resource.Enable.Value;
}
if (resource.AppProfileId.HasValue)
{
indexer.AppProfileId = resource.AppProfileId.Value;
}
if (resource.Priority.HasValue)
{
indexer.Priority = resource.Priority.Value;
}
if (resource.Tags != null)
{
var newTags = resource.Tags;
var applyTags = resource.ApplyTags;
switch (applyTags)
{
case ApplyTags.Add:
newTags.ForEach(t => indexer.Tags.Add(t));
break;
case ApplyTags.Remove:
newTags.ForEach(t => indexer.Tags.Remove(t));
break;
case ApplyTags.Replace:
indexer.Tags = new HashSet<int>(newTags);
break;
}
}
}
_indexerFactory.Update(indexersToUpdate);
var indexers = _indexerFactory.All();
foreach (var definition in indexers)
{
_indexerFactory.SetProviderCharacteristics(definition);
}
return Accepted(_resourceMapper.ToResource(indexers));
}
[HttpDelete]
[Consumes("application/json")]
public object DeleteIndexers([FromBody] IndexerEditorResource resource)
{
_indexerFactory.Delete(resource.IndexerIds);
return new { };
}
}
}

View File

@@ -1,21 +0,0 @@
using System.Collections.Generic;
namespace Prowlarr.Api.V1.Indexers
{
public class IndexerEditorResource
{
public List<int> IndexerIds { get; set; }
public bool? Enable { get; set; }
public int? AppProfileId { get; set; }
public int? Priority { get; set; }
public List<int> Tags { get; set; }
public ApplyTags ApplyTags { get; set; }
}
public enum ApplyTags
{
Add,
Remove,
Replace
}
}

View File

@@ -0,0 +1,12 @@
using NzbDrone.Core.Notifications;
namespace Prowlarr.Api.V1.Notifications
{
public class NotificationBulkResource : ProviderBulkResource<NotificationBulkResource>
{
}
public class NotificationBulkResourceMapper : ProviderBulkResourceMapper<NotificationBulkResource, NotificationDefinition>
{
}
}

View File

@@ -1,16 +1,31 @@
using System;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Notifications;
using Prowlarr.Http;
namespace Prowlarr.Api.V1.Notifications
{
[V1ApiController]
public class NotificationController : ProviderControllerBase<NotificationResource, INotification, NotificationDefinition>
public class NotificationController : ProviderControllerBase<NotificationResource, NotificationBulkResource, INotification, NotificationDefinition>
{
public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper();
public static readonly NotificationResourceMapper ResourceMapper = new ();
public static readonly NotificationBulkResourceMapper BulkResourceMapper = new ();
public NotificationController(NotificationFactory notificationFactory)
: base(notificationFactory, "notification", ResourceMapper)
: base(notificationFactory, "notification", ResourceMapper, BulkResourceMapper)
{
}
[NonAction]
public override ActionResult<NotificationResource> UpdateProvider([FromBody] NotificationBulkResource providerResource)
{
throw new NotImplementedException();
}
[NonAction]
public override object DeleteProviders([FromBody] NotificationBulkResource resource)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using NzbDrone.Core.ThingiProvider;
namespace Prowlarr.Api.V1
{
public class ProviderBulkResource<T>
{
public List<int> Ids { get; set; }
public List<int> Tags { get; set; }
public ApplyTags ApplyTags { get; set; }
public ProviderBulkResource()
{
Ids = new List<int>();
}
}
public enum ApplyTags
{
Add,
Remove,
Replace
}
public class ProviderBulkResourceMapper<TProviderBulkResource, TProviderDefinition>
where TProviderBulkResource : ProviderBulkResource<TProviderBulkResource>, new()
where TProviderDefinition : ProviderDefinition, new()
{
public virtual List<TProviderDefinition> UpdateModel(TProviderBulkResource resource, List<TProviderDefinition> existingDefinitions)
{
if (resource == null)
{
return new List<TProviderDefinition>();
}
return existingDefinitions;
}
}
}

View File

@@ -10,18 +10,25 @@ using Prowlarr.Http.REST;
namespace Prowlarr.Api.V1
{
public abstract class ProviderControllerBase<TProviderResource, TProvider, TProviderDefinition> : RestController<TProviderResource>
public abstract class ProviderControllerBase<TProviderResource, TBulkProviderResource, TProvider, TProviderDefinition> : RestController<TProviderResource>
where TProviderDefinition : ProviderDefinition, new()
where TBulkProviderResource : ProviderBulkResource<TBulkProviderResource>, new()
where TProvider : IProvider
where TProviderResource : ProviderResource<TProviderResource>, new()
{
protected readonly IProviderFactory<TProvider, TProviderDefinition> _providerFactory;
protected readonly ProviderResourceMapper<TProviderResource, TProviderDefinition> _resourceMapper;
private readonly ProviderBulkResourceMapper<TBulkProviderResource, TProviderDefinition> _bulkResourceMapper;
protected ProviderControllerBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper)
protected ProviderControllerBase(IProviderFactory<TProvider,
TProviderDefinition> providerFactory,
string resource,
ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper,
ProviderBulkResourceMapper<TBulkProviderResource, TProviderDefinition> bulkResourceMapper)
{
_providerFactory = providerFactory;
_resourceMapper = resourceMapper;
_bulkResourceMapper = bulkResourceMapper;
SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique");
@@ -92,6 +99,47 @@ namespace Prowlarr.Api.V1
return Accepted(providerResource.Id);
}
[HttpPut("bulk")]
[Consumes("application/json")]
[Produces("application/json")]
public virtual ActionResult<TProviderResource> UpdateProvider([FromBody] TBulkProviderResource providerResource)
{
if (!providerResource.Ids.Any())
{
throw new BadRequestException("ids must be provided");
}
var definitionsToUpdate = _providerFactory.Get(providerResource.Ids).ToList();
foreach (var definition in definitionsToUpdate)
{
_providerFactory.SetProviderCharacteristics(definition);
if (providerResource.Tags != null)
{
var newTags = providerResource.Tags;
var applyTags = providerResource.ApplyTags;
switch (applyTags)
{
case ApplyTags.Add:
newTags.ForEach(t => definition.Tags.Add(t));
break;
case ApplyTags.Remove:
newTags.ForEach(t => definition.Tags.Remove(t));
break;
case ApplyTags.Replace:
definition.Tags = new HashSet<int>(newTags);
break;
}
}
}
_bulkResourceMapper.UpdateModel(providerResource, definitionsToUpdate);
return Accepted(_providerFactory.Update(definitionsToUpdate).Select(x => _resourceMapper.ToResource(x)));
}
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate)
{
var definition = _resourceMapper.ToModel(providerResource);
@@ -112,6 +160,16 @@ namespace Prowlarr.Api.V1
return new { };
}
[HttpDelete("bulk")]
[Consumes("application/json")]
[Produces("application/json")]
public virtual object DeleteProviders([FromBody] TBulkProviderResource resource)
{
_providerFactory.Delete(resource.Ids);
return new { };
}
[HttpGet("schema")]
[Produces("application/json")]
public virtual List<TProviderResource> GetTemplates()