elevenlabs_field-1.0.0-beta7/modules/elevenlabs_bytescale/src/Bytescale.php
modules/elevenlabs_bytescale/src/Bytescale.php
<?php namespace Drupal\elevenlabs_bytescale; use Drupal\Component\Uuid\Php; use Drupal\Core\Config\ConfigFactory; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Utility\Token; use Drupal\elevenlabs_bytescale\Form\BytescaleForm; use Drupal\field\Entity\FieldConfig; use Drupal\file\FileStorageInterface; use GuzzleHttp\Client; /** * Bytescale API creator. */ class Bytescale { /** * Array with all instructions. */ protected array $instructions = []; /** * Target field config. */ protected array $targetFieldSettings = []; /** * The entity. */ protected ContentEntityInterface $entity; /** * The http client. */ protected Client $client; /** * The file storage interface. */ protected FileStorageInterface $fileStorage; /** * The file system. */ protected FileSystemInterface $fileSystem; /** * The token system. */ protected Token $token; /** * Account ID. */ private string $accountId; /** * API Key. */ private string $apiKey; /** * The base path. */ private string $basePath = 'https://api.bytescale.com/v2/'; /** * CDN endpoint. */ private string $cdnEndpoint = 'https://upcdn.io/'; /** * The UUID system. */ protected Php $uuid; /** * Constructs a new Bytescale object. * * @param \GuzzleHttp\Client $client * Http client. * @param \Drupal\Core\Config\ConfigFactory $configFactory * The config factory. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityType * The entity type manager interface. * @param \Drupal\Core\File\FileSystemInterface $fileSystem * The file system interface. * @param \Drupal\Component\Uuid\Php $uuid * The uuid system. * @param \Drupal\Core\Utility\Token $token * The token system. */ public function __construct(Client $client, ConfigFactory $configFactory, EntityTypeManagerInterface $entityType, FileSystemInterface $fileSystem, Php $uuid, Token $token) { $this->client = $client; $config = $configFactory->get(BytescaleForm::CONFIG_NAME); $this->apiKey = $config->get('api_key') ?? ''; $this->accountId = $config->get('account_id') ?? ''; $this->fileStorage = $entityType->getStorage('file'); $this->fileSystem = $fileSystem; $this->uuid = $uuid; $this->token = $token; } /** * Create an audio file from a field value. * * @param mixed $fieldValues * The elevenlabs field values. * @param string $originFieldName * The field name to save from. * @param string $targetFieldName * The field name to save to. * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity to save to. * * @return \Drupal\file\Entity\File * The file entity. */ public function concatenateAudios($fieldValues, $originFieldName, $targetFieldName, ContentEntityInterface $entity) { if (empty($fieldValues[0])) { return NULL; } $this->entity = $entity; $this->setInstructions($fieldValues); $this->ensureTargetFieldSettings($targetFieldName); $this->uploadFiles(); $this->mergeFiles(); return $this->generateFileEntity(); } /** * Create instructions. * * @param mixed $fieldValues * The elevenlabs field values. */ private function setInstructions($fieldValues) { foreach ($fieldValues as $value) { if (is_array($value) && !empty($value['target_id'])) { /** @var \Drupal\file\Entity\File */ $file = $this->fileStorage->load($value['target_id']); $path = $this->fileSystem->realpath($file->getFileUri()); $this->instructions[$value['target_id']] = [ 'filePath' => $path, ]; } } $this->instructions = array_values($this->instructions); } /** * Ensures the target field config. * * @param string $fieldName * The field name to save to. */ private function ensureTargetFieldSettings($fieldName) { $config = FieldConfig::loadByName($this->entity->getEntityTypeId(), $this->entity->bundle(), $fieldName); $this->targetFieldSettings = $config->getSettings(); } /** * Upload the files. */ private function uploadFiles() { $i = 0; foreach ($this->instructions as $id => $instruction) { // Generate a master first. if (!$i) { $this->deleteFileById('master'); $this->uploadFile('master', $instruction['filePath']); } $this->uploadFile($id, $instruction['filePath']); $i++; } } /** * Merge the files. */ private function mergeFiles() { $i = 0; $masterFile = $this->getCdnUrl('master', 'save/audio'); $query[] = 'f=mp3'; $options['headers']['content-type'] = 'application/json'; // Generate all the files. foreach ($this->instructions as $id => $instruction) { if ($i) { $query[] = 'append=' . $this->getFilePath($id); } $i++; } $payload = json_encode([ 'destination' => [ 'fileName' => "final.mp3", 'folderPath' => $this->getFolderPath(), ], ]); $response = json_decode($this->makeRequest($masterFile, $query, 'POST', $payload, $options, FALSE), TRUE); $this->waitForAudioJob($response['jobId']); } /** * Generates the file entity. * * @return \Drupal\file\Entity\File * The file entity. */ protected function generateFileEntity() { $fileName = 'final.mp3'; $tmpFile = $this->generateUniqueFile(); file_put_contents($tmpFile, $this->downloadFile('final')); $dir = $this->token->replace($this->targetFieldSettings['uri_scheme'] . '://' . rtrim($this->targetFieldSettings['file_directory'], '/')); $this->fileSystem->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY); $destination = $dir . '/' . $fileName; $destination = $this->fileSystem->copy($tmpFile, $destination, FileSystemInterface::EXISTS_RENAME); /** @var \Drupal\file\Entity\File */ $file = $this->fileStorage->create([ 'uri' => $destination, ]); $file->setPermanent(); $file->save(); // Cleanup. unlink($tmpFile); return $file; } /** * Wait for job. * * @param string $jobId * The job id. */ private function waitForAudioJob($jobId) { while (TRUE) { $response = $this->getAudioJob($jobId); if (!in_array($response['status'], [ 'Pending', 'Cancelling', 'Running', ])) { break; } sleep(1); } } /** * Upload one file. * * @param int $id * The fid of the file. * @param string $path * The path of the file. */ private function uploadFile($id, $path) { $options['headers']['content-type'] = 'audio/mpeg'; $query[] = 'filename=' . $id . '.mp3'; $query[] = 'folderPath=' . $this->getFolderPath(); $query[] = 'filePath=' . $this->getFolderPath() . '/' . $id . '.mp3'; $headers = get_headers($this->getCdnUrl($id), 1); // Skip if it is already uploaded. if (strpos($headers[0], '200 OK') === FALSE) { $this->makeRequest('accounts/' . $this->accountId . '/uploads/binary', $query, 'POST', file_get_contents($path), $options); } } /** * Download one file. * * @param string $id * The fid of the file. */ private function downloadFile($id) { $file = $this->getCdnUrl($id); return file_get_contents($file); } /** * Get status of a job. * * @param string $jobId * The job id. * * @return array * The job response. */ private function getAudioJob($jobId) { $response = $this->makeRequest('accounts/' . $this->accountId . '/jobs/ProcessAudioJob/' . $jobId); return json_decode($response, TRUE); } /** * Delete one file by id. * * @param int $id * The fid of the file. */ private function deleteFileById($id) { $path = $this->getFolderPath() . '/' . $id . '.mp3'; $this->deleteFile($path); } /** * Delete one file. * * @param string $path * The path of the file. */ private function deleteFile($path) { $query[] = 'filePath=' . $path; $this->makeRequest('accounts/' . $this->accountId . '/files', $query, 'DELETE'); } /** * Get cdn url of file. * * @param int $id * The fid of the file. * @param string $type * The type of file to get. */ private function getCdnUrl($id, $type = "raw") { return $this->cdnEndpoint . $this->accountId . "/$type/" . $this->entity->getEntityTypeId() . '-' . $this->entity->id() . '/' . $id . '.mp3'; } /** * Get path of file. * * @param int $id * The fid of the file. */ private function getFilePath($id) { return $this->getFolderPath() . '/' . $id . '.mp3'; } /** * Get folder path. */ private function getFolderPath() { return '/' . $this->entity->getEntityTypeId() . '-' . $this->entity->id(); } /** * Generate tmp file. * * @param string $ext * The extension. * * @return string * The file path. */ private function generateUniqueFile($ext = 'mp3') { $tmpFile = $this->fileSystem->getTempDirectory() . '/' . $this->uuid->generate() . '.' . $ext; $this->tmpFiles[] = $tmpFile; return $tmpFile; } /** * Make Bytescale call. * * @param string $path * The path. * @param array $queryString * The query string. * @param string $method * The method. * @param string $body * Data to attach if POST/PUT/PATCH. * @param array $options * Extra headers. * @param bool $basePath * If the base path should be added. * * @return string|object * The return response. */ protected function makeRequest($path, array $queryString = [], $method = 'GET', $body = '', array $options = [], $basePath = TRUE) { // We can wait some. $options['connect_timeout'] = 30; $options['read_timeout'] = 30; // Don't let Guzzle die, just forward body and status. $options['http_errors'] = FALSE; // Headers. $options['headers']['Authorization'] = 'Bearer ' . $this->apiKey; if ($body) { $options['body'] = $body; } $newUrl = $basePath ? $this->basePath . $path : $path; $newUrl .= count($queryString) ? '?' . implode('&', $queryString) : ''; $res = $this->client->request($method, $newUrl, $options); return $res->getBody(); } }