Files
feadulta/mu-plugins/fea-subir-audio-api.php
T

198 lines
6.9 KiB
PHP

<?php
/**
* Plugin Name: Fe Adulta — API subir audio TTS
* Description: Endpoint REST acotado para publicar el MP3 TTS de un artículo.
* Version: 1.0
*
* POST /wp-json/fea/v1/subir-audio
* multipart/form-data: post_id=<id>, audio=<MP3>, voice_id=<opcional>
*
* Ver issue gitea.feadulta.com/rafa/feadulta#222.
*/
if (!defined('ABSPATH')) exit;
const FEA_AUDIO_MAX_BYTES = 26214400; // 25 MiB: límite operativo de producción.
add_action('rest_api_init', function () {
register_rest_route('fea/v1', '/subir-audio', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'fea_subir_audio_handle',
'permission_callback' => 'fea_subir_audio_can_call',
// La validación de post_id ocurre en el handler, después de autorizar:
// una llamada anónima siempre recibe 401 aunque omita parámetros.
'args' => [
'post_id' => [
'sanitize_callback' => 'absint',
],
'voice_id' => [
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
});
/** Editor o superior: mismo nivel que los endpoints de escritura existentes. */
function fea_subir_audio_can_call(WP_REST_Request $request) {
if (!is_user_logged_in()) {
return new WP_Error(
'fea_subir_audio_not_authenticated',
'Debes autenticarte para subir un audio.',
['status' => 401]
);
}
if (!current_user_can('edit_others_posts')) {
return new WP_Error(
'fea_subir_audio_forbidden',
'No tienes permiso para subir audios.',
['status' => 403]
);
}
return true;
}
function fea_subir_audio_handle(WP_REST_Request $request) {
$post_id = absint($request->get_param('post_id'));
if (!$post_id) {
return new WP_Error(
'fea_subir_audio_invalid_post',
'post_id es obligatorio.',
['status' => 400]
);
}
if (!get_post($post_id)) {
return new WP_Error(
'fea_subir_audio_post_not_found',
'No existe el artículo indicado.',
['status' => 404]
);
}
$files = $request->get_file_params();
$file = $files['audio'] ?? null;
$validation = fea_subir_audio_validate_file($file);
if (is_wp_error($validation)) return $validation;
$previous = [
'url' => get_post_meta($post_id, 'fea_audio_url', true) ?: null,
'voice' => get_post_meta($post_id, 'fea_audio_voice', true) ?: null,
'done' => get_post_meta($post_id, 'fea_audio_done', true) ?: null,
'sha256' => get_post_meta($post_id, 'fea_audio_sha256', true) ?: null,
'error' => get_post_meta($post_id, 'fea_audio_error', true) ?: null,
];
require_once ABSPATH . 'wp-admin/includes/file.php';
$upload_dir_filter = 'fea_subir_audio_upload_dir';
add_filter('upload_dir', $upload_dir_filter);
try {
$uploaded = wp_handle_upload($file, [
'test_form' => false,
'mimes' => ['mp3' => 'audio/mpeg'],
'unique_filename_callback' => function ($dir, $name, $ext) use ($post_id) {
return $post_id . '.mp3';
},
]);
} finally {
remove_filter('upload_dir', $upload_dir_filter);
}
if (!empty($uploaded['error']) || empty($uploaded['file'])) {
return new WP_Error(
'fea_subir_audio_upload_failed',
'No se pudo guardar el audio: ' . ($uploaded['error'] ?? 'error desconocido'),
['status' => 400]
);
}
$sha256 = hash_file('sha256', $uploaded['file']);
if ($sha256 === false) {
wp_delete_file($uploaded['file']);
return new WP_Error(
'fea_subir_audio_hash_failed',
'No se pudo calcular la huella del audio subido.',
['status' => 500]
);
}
// El path es estable para que los players y el proceso de backfill no dependan
// de las carpetas por mes que normalmente crea WordPress.
$audio_url = home_url('/wp-content/uploads/tts/' . $post_id . '.mp3');
update_post_meta($post_id, 'fea_audio_url', $audio_url);
update_post_meta($post_id, 'fea_audio_voice', (string) $request->get_param('voice_id'));
update_post_meta($post_id, 'fea_audio_done', '1');
update_post_meta($post_id, 'fea_audio_sha256', $sha256);
delete_post_meta($post_id, 'fea_audio_error');
return new WP_REST_Response([
'post_id' => $post_id,
'audio_url' => $audio_url,
'voice_id' => (string) $request->get_param('voice_id'),
'sha256' => $sha256,
'previous_audio' => $previous,
], 201);
}
/** Guarda el binario en uploads/tts sin cambiar globalmente la configuración. */
function fea_subir_audio_upload_dir($uploads) {
$uploads['subdir'] = '/tts';
$uploads['path'] = $uploads['basedir'] . $uploads['subdir'];
$uploads['url'] = $uploads['baseurl'] . $uploads['subdir'];
if (!wp_mkdir_p($uploads['path'])) {
$uploads['error'] = 'No se pudo crear el directorio de audios.';
}
return $uploads;
}
function fea_subir_audio_validate_file($file) {
if (!is_array($file) || empty($file['tmp_name']) || !isset($file['error'])) {
return new WP_Error(
'fea_subir_audio_missing_file',
'audio es obligatorio.',
['status' => 400]
);
}
if ((int) $file['error'] !== UPLOAD_ERR_OK) {
return new WP_Error(
'fea_subir_audio_file_error',
'La subida de audio falló.',
['status' => 400]
);
}
if ((int) $file['size'] <= 0 || (int) $file['size'] > FEA_AUDIO_MAX_BYTES) {
return new WP_Error(
'fea_subir_audio_file_too_large',
'El audio debe ocupar entre 1 byte y 25 MiB.',
['status' => 400]
);
}
$type = wp_check_filetype((string) ($file['name'] ?? ''), ['mp3' => 'audio/mpeg']);
if (($type['ext'] ?? '') !== 'mp3' || ($type['type'] ?? '') !== 'audio/mpeg') {
return new WP_Error(
'fea_subir_audio_unsupported_type',
'Solo se admiten archivos MP3 (audio/mpeg).',
['status' => 400]
);
}
if (strtolower((string) ($file['type'] ?? '')) !== 'audio/mpeg') {
return new WP_Error(
'fea_subir_audio_unsupported_type',
'audio debe declararse como audio/mpeg.',
['status' => 400]
);
}
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = $finfo ? finfo_file($finfo, $file['tmp_name']) : false;
if ($finfo) finfo_close($finfo);
if ($mime !== false && $mime !== 'audio/mpeg') {
return new WP_Error(
'fea_subir_audio_invalid_audio',
'audio debe contener datos MPEG válidos.',
['status' => 400]
);
}
}
return true;
}