using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using JD.Adobe.Service.API; using Plume.Core.Logging; using Plume.Interfaces.Services; using Shuttle.Api.AfterEffectsDTO; using Shuttle.Api.AfterEffectsDTO.Items; using Shuttle.Api.AfterEffectsDTO.Render; using Shuttle.Api.AfterEffectsDTO.UpdatedData; using Shuttle.Api.CommunicationDTO; using Shuttle.Api.ServerAPI; using Shuttle.Servers; using Shuttle.Servers.Managers.Items; using Shuttle.Servers.Managers.Notifications; namespace JD.Adobe.Service.Implementation { /// /// Service to interact with After Effects. /// public class AfterEffectsService : AdobeService, IAfterEffectsService { private IAfterEffectsOperationExecutor _operationExecutor; private IAfterEffectsItemsProvider _itemProvider; private const int RootItemId = 0; public AfterEffectsService(ILogger logger) : base(logger, ServerConstants.ShuttleServerUrl) { } protected override void InitializeImpl(IServiceContext context) { if (_isInitialized) { throw new InvalidOperationException($"{nameof(AdobeService)} is already initialized."); } _itemProvider = new AfterEffectsItemProvider(Logger, _clientManager, _asyncProvider); _operationExecutor = new AfterEffectsOperationExecutor(Logger, _clientManager, _asyncProvider); _isInitialized = true; } public override async Task StartNewInstance(AdobeApplicationType appType) { string requestToken = _asyncProvider.CreateResourceRequest("start-new-instance"); // define a local function to capture the token and avoid keeping a state in the service void EnsureEmptyProject(TargetAppInfo targetAppInfo) { if (targetAppInfo.AppType != appType) { return; } string currentFilePath = this.GetCurrentProjectFilePath(targetAppInfo).GetAwaiter().GetResult(); // make the call synchronously because the event itself will be raised in a Task if (currentFilePath == string.Empty) { // the new instance just connected :) _asyncProvider.DepositResource(requestToken, targetAppInfo); } else { Logger.Debug($"{targetAppInfo} connected but didn't have an empty project file: {currentFilePath}"); } } _clientManager.OnTargetConnection += EnsureEmptyProject; try { if (!_adobeExecutableManager.TryStartNewInstance(appType)) { throw new ArgumentException($"Couldn't start a new instance of {appType}. Are you sure this Adobe application is installed on your machine?"); } int waitingTime = 60_000; bool success = await _asyncProvider.Wait(requestToken, waitingTime); if (!success) { throw new TimeoutException($"{appType} took more than {waitingTime}ms to launch and establish a connection. " + $"You may need to disable the Home startup menu from Edit > Preferences > General."); } } finally { _clientManager.OnTargetConnection -= EnsureEmptyProject; } TargetAppInfo startedAppInfo = _asyncProvider.GetResource(requestToken); return startedAppInfo; } // TASKS public async Task PurgeCache(TargetAppInfo target) { ThrowIfUnknown(target); await _operationExecutor.PurgeMemoryAsync(target.ConnectionId); } public async Task Save(TargetAppInfo target) { ThrowIfUnknown(target); await _operationExecutor.SaveProjectAsync(target.ConnectionId); } public async Task SaveAs(TargetAppInfo target, string filePath) { ThrowIfUnknown(target); if (string.IsNullOrEmpty(filePath)) { throw new ArgumentException($"{nameof(filePath)} cannot be null or empty.", nameof(filePath)); } await _operationExecutor.SaveProjectAsync(target.ConnectionId, filePath); } public async Task OpenProject(TargetAppInfo target, string projectFilePath) { ThrowIfUnknown(target); if (!File.Exists(projectFilePath)) { throw new ArgumentException($"{nameof(projectFilePath)} doesn't exist at '{projectFilePath}'."); } await _operationExecutor.OpenProject(target.ConnectionId, projectFilePath); } public async Task ShowRenderWindow(TargetAppInfo target, bool show) { ThrowIfUnknown(target); await _operationExecutor.ShowRenderWindow(target.ConnectionId, show); } public async Task BeginUndoGroup(TargetAppInfo target, string undoGroupName) { ThrowIfUnknown(target); if (string.IsNullOrEmpty(undoGroupName)) { throw new ArgumentException($"{nameof(undoGroupName)} cannot be null or empty.", nameof(undoGroupName)); } await _operationExecutor.BeginUndoGroup(target.ConnectionId, undoGroupName); } public async Task EndUndoGroup(TargetAppInfo target) { ThrowIfUnknown(target); await _operationExecutor.EndUndoGroup(target.ConnectionId); } public async Task GetCurrentProjectFilePath(TargetAppInfo target) { ThrowIfUnknown(target); string filename = await _itemProvider.GetCurrentFileAsync(target.ConnectionId); return filename; } // GET ITEMS public async Task> GetItems(TargetAppInfo target) { ThrowIfUnknown(target); ICollection items = await _itemProvider.GetAllItemsAsync(target.ConnectionId); return items; } public async Task> GetSelectedItems(TargetAppInfo target) { ThrowIfUnknown(target); ICollection items = await _itemProvider.GetSelectedProjectItemsAsync(target.ConnectionId); return items; } public async Task GetRenderQueue(TargetAppInfo target) { ThrowIfUnknown(target); RenderQueue renderQueue = await _itemProvider.GetRenderQueueAsync(target.ConnectionId); return renderQueue; } public async Task GetRenderQueueItem(TargetAppInfo target, int index) { ThrowIfUnknown(target); RenderQueueItem renderQueueItem = await _itemProvider.GetRenderQueueItemAsync(target.ConnectionId, index); return renderQueueItem; } public async Task GetFolderItem(TargetAppInfo target, int folderId) { ThrowIfUnknown(target); FolderItem folderItem = await _itemProvider.GetFolderItemAsync(target.ConnectionId, folderId); return folderItem; } public async Task GetCompItem(TargetAppInfo target, int compositionId) { ThrowIfUnknown(target); CompItem compItem = await _itemProvider.GetCompItemAsync(target.ConnectionId, compositionId); return compItem; } public async Task GetFootageItem(TargetAppInfo target, int footageId) { ThrowIfUnknown(target); FootageItem footageItem = await _itemProvider.GetFootageItemAsync(target.ConnectionId, footageId); return footageItem; } // CREATE ITEMS public async Task AddFolder(TargetAppInfo target, string name) { return await AddFolder(target, name, RootItemId); } public async Task AddFolder(TargetAppInfo target, string name, int parentFolderId) { ThrowIfUnknown(target); if (name == null) { throw new ArgumentNullException(nameof(name)); } if (parentFolderId < 0) { throw new ArgumentOutOfRangeException(nameof(parentFolderId), $"{nameof(parentFolderId)} must be non-negative."); } FolderItem folderItem = await _itemProvider.CreateFolderAsync(target.ConnectionId, name, parentFolderId); return folderItem; } public async Task AddComp(TargetAppInfo target, string name, int width, int height, float pixelAspect, float duration, float frameRate) { return await AddComp(target, name, width, height, pixelAspect, duration, frameRate, RootItemId); } public async Task AddComp(TargetAppInfo target, string name, int width, int height, float pixelAspect, float duration, float frameRate, int parentFolderId) { ThrowIfUnknown(target); if (name == null) { throw new ArgumentNullException(nameof(name)); } if (width < 0) throw new ArgumentOutOfRangeException(nameof(width), $"{nameof(width)} must be non-negative."); if (height < 0) throw new ArgumentOutOfRangeException(nameof(height), $"{nameof(height)} must be non-negative."); if (pixelAspect < 0) throw new ArgumentOutOfRangeException(nameof(pixelAspect), $"{nameof(pixelAspect)} must be non-negative."); if (duration < 0) throw new ArgumentOutOfRangeException(nameof(duration), $"{nameof(duration)} must be non-negative."); if (frameRate < 0) throw new ArgumentOutOfRangeException(nameof(frameRate), $"{nameof(frameRate)} must be non-negative."); if (parentFolderId < 0) { throw new ArgumentOutOfRangeException(nameof(parentFolderId), $"{nameof(parentFolderId)} must be non-negative."); } CompSettings settings = new CompSettings() { Width = width, Height = height, PixelAspect = pixelAspect, Duration = duration, FrameRate = frameRate }; CompItem compItem = await _itemProvider.CreateCompAsync(target.ConnectionId, name, settings, parentFolderId); return compItem; } public async Task ImportFile(TargetAppInfo target, string footageFilePath) { return await ImportFile(target, footageFilePath, RootItemId); } public async Task ImportFile(TargetAppInfo target, string footageFilePath, int parentFolderId) { ThrowIfUnknown(target); if (!File.Exists(footageFilePath)) { throw new ArgumentException($"{nameof(footageFilePath)} doesn't exist at '{footageFilePath}'."); } if (parentFolderId < 0) { throw new ArgumentOutOfRangeException(nameof(parentFolderId), $"{nameof(parentFolderId)} must be non-negative."); } Item footageItem = await _itemProvider.CreateFootageAsync(target.ConnectionId, footageFilePath, parentFolderId); return footageItem; } public async Task AddToRenderQueue(TargetAppInfo target, int compId) { ThrowIfUnknown(target); if (compId <= 0) { throw new ArgumentOutOfRangeException(nameof(compId), $"{nameof(compId)} must be strictly positive."); } RenderQueueItem renderQueueItem = await _itemProvider.AddToRenderQueue(target.ConnectionId, compId); return renderQueueItem; } public async Task AddNullLayer(TargetAppInfo target, int compId, string layerName) { ThrowIfUnknown(target); if (compId <= 0) { throw new ArgumentOutOfRangeException(nameof(compId), $"{nameof(compId)} must be strictly positive."); } if (layerName == null) { throw new ArgumentNullException(nameof(layerName)); } Layer newLayer = await _itemProvider.AddNullLayerAsync(target.ConnectionId, compId, layerName); return newLayer; } public async Task AddLayer(TargetAppInfo target, int compId, int sourceItemId) { ThrowIfUnknown(target); if (compId <= 0) { throw new ArgumentOutOfRangeException(nameof(compId), $"{nameof(compId)} must be strictly positive."); } if (sourceItemId <= 0) { throw new ArgumentOutOfRangeException(nameof(sourceItemId), $"{nameof(sourceItemId)} must be strictly positive."); } Layer newLayer = await _itemProvider.AddLayerAsync(target.ConnectionId, compId, sourceItemId); return newLayer; } // UPDATE ITEMS public async Task UpdateFolderItem(TargetAppInfo target, int folderItemId, FolderItemUpdatableData folderItemData) { ThrowIfUnknown(target); if (folderItemData == null) { throw new ArgumentNullException(nameof(folderItemData)); } if (folderItemId <= 0) { throw new ArgumentOutOfRangeException(nameof(folderItemId), $"{nameof(folderItemId)} must be strictly positive."); } FolderItem updatedFolderItem = await _itemProvider.UpdateFolderAsync(target.ConnectionId, folderItemId, folderItemData); return updatedFolderItem; } public async Task UpdateCompItem(TargetAppInfo target, int compItemId, CompItemUpdatableData compItemData) { ThrowIfUnknown(target); if (compItemData == null) { throw new ArgumentNullException(nameof(compItemData)); } if (compItemId <= 0) { throw new ArgumentOutOfRangeException(nameof(compItemId), $"{nameof(compItemId)} must be strictly positive."); } CompItem updatedCompItem = await _itemProvider.UpdateCompAsync(target.ConnectionId, compItemId, compItemData); return updatedCompItem; } public async Task CopyLayers(TargetAppInfo target, int sourceCompId, int targetCompId) { ThrowIfUnknown(target); if (sourceCompId <= 0) { throw new ArgumentOutOfRangeException(nameof(sourceCompId), $"{nameof(sourceCompId)} must be strictly positive."); } if (targetCompId <= 0) { throw new ArgumentOutOfRangeException(nameof(targetCompId), $"{nameof(targetCompId)} must be strictly positive."); } CompItem updatedCompItem = await _itemProvider.CopyLayersAsync(target.ConnectionId, sourceCompId, targetCompId); return updatedCompItem; } public async Task RemoveLayers(TargetAppInfo target, int compId, ICollection layerNames) { ThrowIfUnknown(target); if (compId <= 0) { throw new ArgumentOutOfRangeException(nameof(compId), $"{nameof(compId)} must be strictly positive."); } if (layerNames == null) { throw new ArgumentNullException(nameof(layerNames)); } if (layerNames.Count == 0) { return await _itemProvider.GetCompItemAsync(target.ConnectionId, compId); } CompItem updatedCompItem = await _itemProvider.RemoveLayersAsync(target.ConnectionId, compId, layerNames); return updatedCompItem; } public async Task UpdateFootageItem(TargetAppInfo target, int footageItemId, FootageItemUpdatableData footageItemData) { ThrowIfUnknown(target); if (footageItemData == null) { throw new ArgumentNullException(nameof(footageItemData)); } if (footageItemId <= 0) { throw new ArgumentOutOfRangeException(nameof(footageItemId), $"{nameof(footageItemId)} must be strictly positive."); } FootageItem updateFootageItem = await _itemProvider.UpdateFootageAsync(target.ConnectionId, footageItemId, footageItemData); return updateFootageItem; } public async Task ReplaceFootageSource(TargetAppInfo target, int footageId, string sourceFilePath) { ThrowIfUnknown(target); if (footageId <= 0) { throw new ArgumentOutOfRangeException(nameof(footageId), $"{nameof(footageId)} must be strictly positive."); } if (!File.Exists(sourceFilePath)) { throw new ArgumentException($"{nameof(sourceFilePath)} doesn't exist at '{sourceFilePath}'."); } FootageItem updateFootageItem = await _itemProvider.ReplaceFootageSourceAsync(target.ConnectionId, footageId, sourceFilePath); return updateFootageItem; } public async Task ReplaceFootageSourceWithSequence(TargetAppInfo target, int footageId, string firstSourceFilePath, bool forceAlphabetical) { ThrowIfUnknown(target); if (footageId <= 0) { throw new ArgumentOutOfRangeException(nameof(footageId), $"{nameof(footageId)} must be strictly positive."); } if (!File.Exists(firstSourceFilePath)) { throw new ArgumentException($"{nameof(firstSourceFilePath)} doesn't exist at '{firstSourceFilePath}'."); } FootageItem updateFootageItem = await _itemProvider.ReplaceFootageSourceWithSequenceAsync(target.ConnectionId, footageId, firstSourceFilePath, forceAlphabetical); return updateFootageItem; } public async Task UpdateRenderQueueItem(TargetAppInfo target, int renderQueueItemId, RenderQueueItem renderQueueItem) { ThrowIfUnknown(target); if (renderQueueItem == null) { throw new ArgumentNullException(nameof(renderQueueItem)); } if (renderQueueItemId <= 0) { throw new ArgumentOutOfRangeException(nameof(renderQueueItemId), $"{nameof(renderQueueItemId)} must be strictly positive."); } RenderQueueItem updateRenderQueueItem = await _itemProvider.UpdateRenderQueueItem(target.ConnectionId, renderQueueItemId, renderQueueItem); return updateRenderQueueItem; } public async Task ConfigureRenderQueueItem(TargetAppInfo target, int renderQueueItemIndex, string settingsTemplateName, string outputModuleTemplateName, string outputFilePath) { ThrowIfUnknown(target); if (renderQueueItemIndex <= 0) { throw new ArgumentOutOfRangeException(nameof(renderQueueItemIndex), $"{nameof(renderQueueItemIndex)} must be strictly positive (indexes start at 1)."); } if (string.IsNullOrEmpty(settingsTemplateName)) { throw new ArgumentException($"{nameof(settingsTemplateName)} cannot be null or empty.", nameof(settingsTemplateName)); } if (string.IsNullOrEmpty(outputModuleTemplateName)) { throw new ArgumentException($"{nameof(outputModuleTemplateName)} cannot be null or empty.", nameof(outputModuleTemplateName)); } if (string.IsNullOrEmpty(outputFilePath)) { throw new ArgumentException($"{nameof(outputFilePath)} cannot be null or empty.", nameof(outputFilePath)); } RenderQueueItem configuredRenderQueueItem = await _itemProvider.ConfigureRenderQueueItem(target.ConnectionId, renderQueueItemIndex, settingsTemplateName, outputModuleTemplateName, outputFilePath); return configuredRenderQueueItem; } public async Task UpdateLayer(TargetAppInfo target, int compId, int layerIndex, LayerUpdatableData layerData) { ThrowIfUnknown(target); if (compId <= 0) { throw new ArgumentOutOfRangeException(nameof(compId), $"{nameof(compId)} must be strictly positive."); } if (layerIndex <= 0) { throw new ArgumentOutOfRangeException(nameof(layerIndex), $"{nameof(layerIndex)} must be strictly positive."); } if (layerData == null) { throw new ArgumentNullException(nameof(layerData)); } Layer updatedLayer = await _itemProvider.UpdateLayerAsync(target.ConnectionId, compId, layerIndex, layerData); return updatedLayer; } public async Task SetParentFolderToRoot(TargetAppInfo target, int itemId) { return await SetParentFolder(target, itemId, RootItemId); } public async Task SetParentFolder(TargetAppInfo target, int itemId, int parentFolderId) { ThrowIfUnknown(target); if (itemId <= 0) { throw new ArgumentOutOfRangeException(nameof(itemId), $"{nameof(itemId)} must be strictly positive."); } if (parentFolderId < 0) { throw new ArgumentOutOfRangeException(nameof(parentFolderId), $"{nameof(parentFolderId)} must be non-negative."); } Item updateItem = await _itemProvider.SetParentAsync(target.ConnectionId, itemId, parentFolderId); return updateItem; } public async Task RenameItem(TargetAppInfo target, int itemId, string newName) { ThrowIfUnknown(target); if (itemId <= 0) { throw new ArgumentOutOfRangeException(nameof(itemId), $"{nameof(itemId)} must be strictly positive."); } if (string.IsNullOrEmpty(newName)) { throw new ArgumentException($"{nameof(newName)} cannot be null or empty.", nameof(newName)); } Item updateItem = await _itemProvider.RenameItem(target.ConnectionId, itemId, newName); return updateItem; } // DELETE ITEMS public async Task RemoveItem(TargetAppInfo target, int itemId) { ThrowIfUnknown(target); if (itemId <= 0) { throw new ArgumentOutOfRangeException(nameof(itemId), $"{nameof(itemId)} must be strictly positive."); } await _itemProvider.RemoveItemFromProject(target.ConnectionId, itemId); } } }