PluginProbe
BeyondWords – AI audio for publishers / 6.0.3
BeyondWords – AI audio for publishers v6.0.3
7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.7.0 All 43 releases
speechkit / src / Core / Core.php

Core.php in BeyondWords – AI audio for publishers 6.0.3, at src/Core/Core.php

438 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Beyondwords\Wordpress\Core;
6
7 use Beyondwords\Wordpress\Component\Post\PostMetaUtils;
8 use Beyondwords\Wordpress\Component\Settings\Fields\IntegrationMethod\IntegrationMethod;
9 use Beyondwords\Wordpress\Component\Settings\SettingsUtils;
10 use Beyondwords\Wordpress\Core\CoreUtils;
11
12 class Core
13 {
14 /**
15 * Init.
16 *
17 * @since 4.0.0
18 * @since 6.0.0 Make static and stop loading plugin text domain on init.
19 */
20 public static function init(): void
21 {
22 // Actions
23 add_action('enqueue_block_editor_assets', [self::class, 'enqueueBlockEditorAssets'], 1, 0);
24 add_action('init', [self::class, 'registerMeta'], 99, 3);
25
26 // Actions for adding/updating posts
27 add_action('wp_after_insert_post', [self::class, 'onAddOrUpdatePost'], 99);
28
29 // Actions for trashing/deleting posts
30 add_action('wp_trash_post', [self::class, 'onTrashPost']);
31 add_action('before_delete_post', [self::class, 'onDeletePost']);
32
33 add_filter('is_protected_meta', [self::class, 'isProtectedMeta'], 10, 2);
34
35 // Older posts may be missing beyondwords_language_code, so we'll try to set it.
36 add_filter('get_post_metadata', [self::class, 'getLangCodeFromJsonIfEmpty'], 10, 3);
37 }
38
39 /**
40 * Should process post status?
41 *
42 * @since 3.5.0
43 * @since 3.7.0 Process audio for posts with 'pending' status
44 * @since 5.0.0 Remove beyondwords_post_statuses filter.
45 * @since 6.0.0 Make static.
46 *
47 * @param string $status WordPress post status (e.g. 'pending', 'publish', 'private', 'future', etc).
48 */
49 public static function shouldProcessPostStatus(string $status): bool
50 {
51 $statuses = ['pending', 'publish', 'private', 'future'];
52
53 /**
54 * Filters the post statuses that we consider for audio processing.
55 *
56 * When a post is saved with any other post status we will not send
57 * any data to the BeyondWords API.
58 *
59 * The default values are "pending", "publish", "private" and "future".
60 *
61 * @since 3.3.3 Introduced as beyondwords_post_statuses.
62 * @since 3.7.0 Process audio for posts with 'pending' status.
63 * @since 4.3.0 Renamed from beyondwords_post_statuses to beyondwords_settings_post_statuses.
64 *
65 * @param string[] $statuses The post statuses that we consider for audio processing.
66 */
67 $statuses = apply_filters('beyondwords_settings_post_statuses', $statuses);
68
69 // Only generate audio for certain post statuses
70 if (is_array($statuses) && in_array($status, $statuses)) {
71 return true;
72 }
73
74 return false;
75 }
76
77 /**
78 * Should generate audio for post?
79 *
80 * @since 3.5.0
81 * @since 3.10.0 Remove wp_is_post_revision check
82 * @since 5.1.0 Regenerate audio for all post statuses
83 * @since 6.0.0 Make static, ignore revisions, refactor status
84 * checks, and add support Magic Embed support.
85 *
86 * @param int $postId WordPress Post ID.
87 */
88 public static function shouldGenerateAudioForPost(int $postId): bool
89 {
90 // Ignore autosaves and revisions
91 if (wp_is_post_autosave($postId) || wp_is_post_revision($postId)) {
92 return false;
93 }
94
95 $status = get_post_status($postId);
96
97 // Only (re)generate audio for certain post statuses.
98 if (! self::shouldProcessPostStatus($status)) {
99 return false;
100 }
101
102 // Generate if the "Generate audio" custom field is set.
103 if (PostMetaUtils::hasGenerateAudio($postId)) {
104 return (bool) get_post_meta($postId, 'beyondwords_generate_audio', true);
105 }
106
107 return false;
108 }
109
110 /**
111 * Generate audio for a post if certain conditions are met.
112 *
113 * @since 3.0.0
114 * @since 3.2.0 Added speechkit_post_statuses filter
115 * @since 3.5.0 Refactored, adding self::shouldGenerateAudioForPost()
116 * @since 5.1.0 Move project ID check into self::shouldGenerateAudioForPost()
117 * @since 6.0.0 Make static and support Magic Embed.
118 *
119 * @param int $postId WordPress Post ID.
120 *
121 * @return array|false|null Response from API, or false if audio was not generated.
122 */
123 public static function generateAudioForPost(int $postId): array|false|null
124 {
125 // Perform checks to see if this post should be processed
126 if (! self::shouldGenerateAudioForPost($postId)) {
127 return false;
128 }
129
130 $post = get_post($postId);
131 if (! $post) {
132 return false;
133 }
134
135 $integrationMethod = IntegrationMethod::getIntegrationMethod($post);
136
137 // For Magic Embed we call the "get_player_by_source_id" endpoint to import content.
138 if (IntegrationMethod::CLIENT_SIDE === $integrationMethod) {
139 // Save the integration method & Project ID.
140 update_post_meta($postId, 'beyondwords_integration_method', IntegrationMethod::CLIENT_SIDE);
141 update_post_meta($postId, 'beyondwords_project_id', get_option('beyondwords_project_id'));
142
143 return ApiClient::getPlayerBySourceId($postId);
144 }
145
146 // For non-Magic Embed we use the REST API to generate audio.
147 update_post_meta($postId, 'beyondwords_integration_method', IntegrationMethod::REST_API);
148
149 // Does this post already have audio?
150 $contentId = PostMetaUtils::getContentId($postId);
151
152 // Has autoregeneration for Post updates been disabled?
153 if ($contentId) {
154 if (defined('BEYONDWORDS_AUTOREGENERATE') && ! BEYONDWORDS_AUTOREGENERATE) {
155 return false;
156 }
157
158 $response = ApiClient::updateAudio($postId);
159 } else {
160 $response = ApiClient::createAudio($postId);
161 }
162
163 $projectId = PostMetaUtils::getProjectId($postId);
164
165 self::processResponse($response, $projectId, $postId);
166
167 return $response;
168 }
169
170 /**
171 * Delete audio for post.
172 *
173 * @since 4.0.5
174 * @since 6.0.0 Make static.
175 *
176 * @param int $postId WordPress Post ID.
177 *
178 * @return array|false|null Response from API, or false if audio was not generated.
179 */
180 public static function deleteAudioForPost(int $postId): array|false|null
181 {
182 return ApiClient::deleteAudio($postId);
183 }
184
185 /**
186 * Batch delete audio for posts.
187 *
188 * @since 4.1.0
189 * @since 6.0.0 Make static.
190 *
191 * @param int[] $postIds Array of WordPress Post IDs.
192 *
193 * @return array|false Response from API, or false if audio was not generated.
194 */
195 public static function batchDeleteAudioForPosts(array $postIds): array|false|null
196 {
197 return ApiClient::batchDeleteAudio($postIds);
198 }
199
200 /**
201 * Process the response body of a BeyondWords REST API response.
202 *
203 * @since 3.0.0
204 * @since 3.7.0 Stop saving response.access_key, we don't currently use it.
205 * @since 4.0.0 Replace Podcast IDs with Content IDs
206 * @since 4.5.0 Save response.preview_token to support post scheduling.
207 * @since 5.0.0 Stop saving `beyondwords_podcast_id`.
208 * @since 6.0.0 Make static.
209 */
210 public static function processResponse(mixed $response, int|string|false $projectId, int $postId): mixed
211 {
212 if (! is_array($response)) {
213 return $response;
214 }
215
216 if ($projectId && ! empty($response['id'])) {
217 update_post_meta($postId, 'beyondwords_project_id', $projectId);
218 update_post_meta($postId, 'beyondwords_content_id', $response['id']);
219
220 if (! empty($response['preview_token'])) {
221 update_post_meta($postId, 'beyondwords_preview_token', $response['preview_token']);
222 }
223
224 if (! empty($response['language'])) {
225 update_post_meta($postId, 'beyondwords_language_code', $response['language']);
226 }
227
228 if (! empty($response['title_voice_id'])) {
229 update_post_meta($postId, 'beyondwords_title_voice_id', $response['title_voice_id']);
230 }
231
232 if (! empty($response['summary_voice_id'])) {
233 update_post_meta($postId, 'beyondwords_summary_voice_id', $response['summary_voice_id']);
234 }
235
236 if (! empty($response['body_voice_id'])) {
237 update_post_meta($postId, 'beyondwords_body_voice_id', $response['body_voice_id']);
238 }
239 }
240
241 return $response;
242 }
243
244 /**
245 * Enqueue Core (built & minified) JS for Block Editor.
246 *
247 * @since 3.0.0
248 * @since 4.5.1 Disable plugin features if we don't have valid API settings.
249 * @since 6.0.0 Make static.
250 */
251 public static function enqueueBlockEditorAssets()
252 {
253 if (! SettingsUtils::hasValidApiConnection()) {
254 return;
255 }
256
257 $postType = get_post_type();
258
259 $postTypes = SettingsUtils::getCompatiblePostTypes();
260
261 if (in_array($postType, $postTypes, true)) {
262 $assetFile = include BEYONDWORDS__PLUGIN_DIR . 'build/index.asset.php';
263
264 // Register the Block Editor JS
265 wp_enqueue_script(
266 'beyondwords-block-js',
267 BEYONDWORDS__PLUGIN_URI . 'build/index.js',
268 $assetFile['dependencies'],
269 $assetFile['version'],
270 true
271 );
272 }
273 }
274
275 /**
276 * Register meta fields for REST API output.
277 *
278 * It is recommended to register meta keys for a specific combination
279 * of object type and object subtype.
280 *
281 * @since 2.5.0
282 * @since 3.9.0 Don't register speechkit_status - downgrades to plugin v2.x are no longer expected.
283 * @since 6.0.0 Make static.
284 **/
285 public static function registerMeta()
286 {
287 $postTypes = SettingsUtils::getCompatiblePostTypes();
288
289 if (is_array($postTypes)) {
290 $keys = CoreUtils::getPostMetaKeys('all');
291
292 foreach ($postTypes as $postType) {
293 $options = [
294 'show_in_rest' => true,
295 'single' => true,
296 'type' => 'string',
297 'default' => '',
298 'object_subtype' => $postType,
299 'prepare_callback' => 'sanitize_text_field',
300 'sanitize_callback' => 'sanitize_text_field',
301 'auth_callback' => fn(): bool => current_user_can('edit_posts'),
302 ];
303
304 foreach ($keys as $key) {
305 register_meta('post', $key, $options);
306 }
307 }
308 }
309 }
310
311 /**
312 * Make all of our custom fields private, so they don't appear in the
313 * "Custom Fields" panel, which can cause conflicts for the Block Editor.
314 *
315 * https://github.com/WordPress/gutenberg/issues/23078
316 *
317 * @since 4.0.0
318 * @since 6.0.0 Make static.
319 * @since 6.0.1 Accept null params from WP core.
320 */
321 public static function isProtectedMeta($protected, $metaKey)
322 {
323 if ($metaKey === null) {
324 return (bool) $protected;
325 }
326
327 $keysToProtect = CoreUtils::getPostMetaKeys('all');
328
329 if (in_array($metaKey, $keysToProtect, true)) {
330 return true;
331 }
332
333 return (bool) $protected;
334 }
335
336 /**
337 * On trash post.
338 *
339 * We attempt to send a DELETE REST API request when a post is trashed so the audio
340 * no longer appears in playlists, or in the publishers BeyondWords dashboard.
341 *
342 * @since 3.9.0 Introduced.
343 * @since 5.4.0 Renamed from onTrashOrDeletePost, and we now remove all
344 * BeyondWords data when a post is trashed.
345 * @since 6.0.0 Make static.
346 *
347 * @param int $postId Post ID.
348 **/
349 public static function onTrashPost($postId)
350 {
351 $postId = (int) $postId;
352 ApiClient::deleteAudio($postId);
353 PostMetaUtils::removeAllBeyondwordsMetadata($postId);
354 }
355
356 /**
357 * On delete post.
358 *
359 * We attempt to send a DELETE REST API request when a post is deleted so the audio
360 * no longer appears in playlists, or in the publishers BeyondWords dashboard.
361 *
362 * @since 5.4.0 Introduced, replacing onTrashOrDeletePost.
363 * @since 6.0.0 Make static.
364 *
365 * @param int $postId Post ID.
366 **/
367 public static function onDeletePost($postId)
368 {
369 $postId = (int) $postId;
370 ApiClient::deleteAudio($postId);
371 }
372
373 /**
374 * WP Save Post action.
375 *
376 * Fires after a post, its terms and meta data has been saved.
377 *
378 * @since 3.0.0
379 * @since 3.2.0 Added beyondwords_post_statuses filter.
380 * @since 3.6.1 Improve $postBefore hash comparison.
381 * @since 3.9.0 Renamed method from wpAfterInsertPost to onAddOrUpdatePost.
382 * @since 4.0.0 Removed hash comparison.
383 * @since 4.4.0 Delete audio if beyondwords_delete_content custom field is set.
384 * @since 4.5.0 Remove unwanted debugging custom fields.
385 * @since 5.1.0 Move post status check out of here.
386 * @since 6.0.0 Make static and refactor for Magic Embed updates.
387 *
388 * @param int $postId Post ID.
389 **/
390 public static function onAddOrUpdatePost($postId)
391 {
392 $postId = (int) $postId;
393
394 // Has the "Remove" feature been used?
395 if (get_post_meta($postId, 'beyondwords_delete_content', true) === '1') {
396 // Make DELETE API request
397 self::deleteAudioForPost($postId);
398
399 // Remove custom fields
400 PostMetaUtils::removeAllBeyondwordsMetadata($postId);
401
402 return false;
403 }
404
405 return (bool) self::generateAudioForPost($postId);
406 }
407
408 /**
409 * Get the language code from a JSON mapping if it is empty.
410 *
411 * @since 5.4.0 Introduced.
412 * @since 6.0.0 Make static.
413 * @since 6.0.1 Accept a null $meta_key parameter.
414 *
415 * @param mixed $value The value of the metadata.
416 * @param int $object_id The ID of the object metadata is for.
417 * @param ?string $meta_key The key of the metadata.
418 *
419 * @return mixed The metadata value.
420 */
421 public static function getLangCodeFromJsonIfEmpty($value, $object_id, $meta_key)
422 {
423 if ('beyondwords_language_code' === $meta_key && empty($value)) {
424 $languageId = get_post_meta($object_id, 'beyondwords_language_id', true);
425
426 if ($languageId) {
427 $langCodes = json_decode(file_get_contents(BEYONDWORDS__PLUGIN_DIR . 'assets/lang-codes.json'), true);
428
429 if (is_array($langCodes) && array_key_exists($languageId, $langCodes)) {
430 return [$langCodes[$languageId]];
431 }
432 }
433 }
434
435 return $value;
436 }
437 }
438