'any', 'post_status' => 'any', 'meta_key' => self::ID_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $video_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'fields' => 'ids', 'numberposts' => 1, ) ); return $posts ? (int) $posts[0] : 0; } /** Posts with a video under way or not yet saved, for the background check. */ public static function pending_posts( $limit = 20 ) { return get_posts( array( 'post_type' => 'any', 'post_status' => 'any', 'meta_key' => self::PENDING_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'fields' => 'ids', 'numberposts' => $limit, 'orderby' => 'modified', ) ); } /** Whether a video still needs something done on this site: followed, or its MP4 saved. */ public static function needs_work( array $video ) { if ( ! in_array( $video['status'], self::FINAL_STATES, true ) ) { return true; } return 'completed' === $video['status'] && empty( $video['attachment_id'] ) && ! Media::gave_up( $video ); } private static function save( $post_id, array $videos ) { update_post_meta( $post_id, self::META, array_values( $videos ) ); $pending = false; foreach ( $videos as $video ) { $pending = $pending || self::needs_work( $video ); } if ( $pending ) { update_post_meta( $post_id, self::PENDING_META, 1 ); } else { delete_post_meta( $post_id, self::PENDING_META ); } } /** Changes one kept video. Answers it as kept, or null when the post has no such video. */ public static function update( $post_id, $video_id, array $changes ) { $videos = self::of_post( $post_id ); foreach ( $videos as $i => $video ) { if ( $video['id'] === $video_id ) { $videos[ $i ] = array_merge( $video, $changes ); self::save( $post_id, $videos ); return $videos[ $i ]; } } return null; } /** What is kept of the API's video. Never its file links: they last an hour. */ private static function from_api( array $video ) { return array( 'id' => $video['id'], 'status' => $video['status'], 'title' => $video['title'], 'format' => $video['format'], 'language' => $video['language'], 'text' => $video['text'], 'seconds' => $video['seconds'], 'steps' => isset( $video['steps'] ) ? $video['steps'] : array(), 'error' => $video['error'], 'credits' => $video['credits'], 'mock' => ! empty( $video['mock'] ), 'app_url' => $video['app_url'], 'created_at' => $video['created_at'], 'finished_at' => $video['finished_at'], 'checked_at' => time(), ); } /** * The API's video (from GET, or a webhook's body) applied to the kept one; a * completed video is then saved to the Media Library in the background. */ public static function apply( $post_id, array $video ) { $kept = self::update( $post_id, $video['id'], self::from_api( $video ) ); if ( $kept && 'completed' === $kept['status'] && empty( $kept['attachment_id'] ) && ! Media::gave_up( $kept ) ) { Media::schedule( $post_id, $kept['id'] ); } return $kept; } /** * Makes a video of the post. The options are the sidebar's (or the post list's), * already checked against GET /options; `request_id` is the press's own, sent as * the Idempotency-Key so a second press of the same request makes nothing new. * * @return array|WP_Error The video as kept. */ public static function create( WP_Post $post, array $options, $request_id ) { $payload = Payload::from_post( $post ); $body = array_merge( array( 'title' => $payload['title'], 'rights_confirmed' => true, 'blocks' => $payload['blocks'], ), $payload['source_url'] ? array( 'source_url' => $payload['source_url'] ) : array(), $options ); if ( ( isset( $options['visuals'] ) && 'single_image' === $options['visuals'] ) || self::brand_kit_single_image( $options ) ) { $image = self::single_image( $post, $payload ); if ( $image ) { $body['image_url'] = $image; } } $video = Connection::call( 'POST', '/videos', array( 'body' => $body, 'headers' => array( 'Idempotency-Key' => 'wp-' . $post->ID . '-' . $request_id ), 'timeout' => 45, ) ); if ( is_wp_error( $video ) ) { return $video; } $kept = self::find( $post->ID, $video['id'] ); if ( $kept ) { return $kept; } $kept = array_merge( self::from_api( $video ), array( 'created_by' => get_current_user_id(), 'review' => ! empty( $options['review_before_render'] ), ) ); $videos = self::of_post( $post->ID ); array_unshift( $videos, $kept ); self::save( $post->ID, $videos ); add_post_meta( $post->ID, self::ID_META, $kept['id'] ); return $kept; } /** * Asks Ojelio again about the post's unfinished videos, at most every * `$min_seconds` each. Answers the list as kept. */ public static function refresh( $post_id, $min_seconds = 5 ) { foreach ( self::of_post( $post_id ) as $kept ) { if ( in_array( $kept['status'], self::FINAL_STATES, true ) || time() - (int) $kept['checked_at'] < $min_seconds ) { continue; } $video = Connection::call( 'GET', '/videos/' . rawurlencode( $kept['id'] ) ); if ( is_wp_error( $video ) ) { if ( 'ojelio_not_found' === $video->get_error_code() ) { self::update( $post_id, $kept['id'], array( 'status' => 'removed', 'checked_at' => time() ) ); } continue; } self::apply( $post_id, $video ); } return self::of_post( $post_id ); } /** Whether the video will be a single picture because the brand kit says so. */ private static function brand_kit_single_image( array $options ) { if ( isset( $options['visuals'] ) ) { return false; } $lists = Settings::options(); return ! is_wp_error( $lists ) && 'single_image' === $lists['defaults']['visuals']; } /** A single-picture video's picture: the featured image, else the post's first picture. */ private static function single_image( WP_Post $post, array $payload ) { $featured = get_the_post_thumbnail_url( $post, 'full' ); if ( $featured && Payload::is_public_url( $featured ) ) { return $featured; } foreach ( $payload['blocks'] as $block ) { if ( 'image' === $block['type'] ) { return $block['url']; } } return null; } }