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();
}
}
