podlove-publisher icon indicating copy to clipboard operation
podlove-publisher copied to clipboard

PodcastEpisode graph piece for Yoast Schema API

Open MarcusWegener opened this issue 2 years ago • 0 comments

It would be nice if Podlove would provide a graph piece for PodcastEpisode for the Yoast Schema API, analogous to SeriouslySimplePodcasting.

https://developer.yoast.com/features/schema/api/#adding-graph-pieces

<?php
/**
 * WPSEO plugin file.
 *
 * @package Yoast\WP\SEO\Generators\Schema
 */

namespace SeriouslySimplePodcasting\Integrations\Yoast\Schema;

use SeriouslySimplePodcasting\Controllers\Frontend_Controller;
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;

/**
 * Returns schema PodcastEpisode data.
 *
 * @since 2.7.3
 */
class PodcastEpisode extends Abstract_Schema_Piece {

	/**
	 * Determines whether PodcastEpisode graph piece should be added.
	 *
	 * @return bool
	 */
	public function is_needed() {
		$ssp_post_types = ssp_post_types( true );

		return is_singular( $ssp_post_types );
	}

	/**
	 * Returns the Podcast Schema data.
	 *
	 * @return array $data The schema data.
	 */
	public function generate() {
		$ss_podcasting = ssp_frontend_controller();

		$series_parts = [];
		$series       = wp_get_post_terms( $this->context->post->ID, 'series' );

		foreach ( $series as $term ) {
			/** @var \WP_Term $term */

			$url = get_term_link( $term );

			if ( is_wp_error( $url ) ) {
				continue;
			}

			$series_parts[] = array(
				"@type" => "PodcastSeries",
				"name"  => $term->name,
				"url"   => $url,
				"id"    => $url . '#/schema/podcastSeries',
			);
		}

		$enclosure   = $ss_podcasting->get_enclosure( $this->context->post->ID );
		$description = get_the_excerpt( $this->context->post->ID );
		$duration    = $this->get_duration( $this->context->post->ID, $ss_podcasting, $enclosure );

		$schema = array(
			"@type"               => [ "PodcastEpisode", "OnDemandEvent" ],
			"@id"                 => $this->context->canonical . '#/schema/podcast',
			"eventAttendanceMode" => "https://schema.org/OnlineEventAttendanceMode",
			"location"            => array(
				"@type" => "VirtualLocation",
				"url"   => $this->context->canonical,
				"@id"   => $this->context->canonical . "#webpage",
			),
			"url"                 => $this->context->canonical,
			"name"                => $this->context->title,
			"datePublished"       => date( 'Y-m-d', strtotime( $this->context->post->post_date ) ),
		);

		if ( $description ) {
			$schema['description'] = $description;
		}

		if ( ! empty( $duration ) ) {
			$schema['duration'] = $duration;
		}

		if ( $enclosure ) {
			$schema = $this->add_enclosure_to_schema( $ss_podcasting, $enclosure, $schema );
		}

		if ( $series_parts ) {
			$schema['partOfSeries'] = $series_parts;
		}

		return $schema;
	}

	/**
	 * Gets a ISO 8601 duration compliant duration string.
	 *
	 * @param int                 $episode_id
	 * @param Frontend_Controller $ss_podcasting
	 * @param string              $enclosure
	 *
	 * @return string
	 */
	protected function get_duration( $episode_id, $ss_podcasting, $enclosure ) {
		$duration = get_post_meta( $episode_id, 'duration', true );
		if ( empty( $duration ) ) {
			$duration = $ss_podcasting->get_file_duration( $enclosure );
			if ( $duration ) {
				update_post_meta( $episode_id, 'duration', $duration );
			}
		}

		preg_match( '/(\d\d:\d\d:\d\d)/', $duration, $matches );

		if ( empty( $matches ) ) {
			return '';
		}

		$time_parts = explode( ':', $duration );

		$hours   = intval( $time_parts[0] );
		$minutes = intval( $time_parts[1] );
		$seconds = intval( $time_parts[2] );

		if ( ( ! $minutes && $seconds ) || $seconds > 30 ) {
			$minutes ++;
		}

		$time = 'P';

		if ( $hours ) {
			$time .= $hours . 'H';
		}
		if ( $minutes ) {
			$time .= $minutes . 'M';
		}

		return $time;
	}

	/**
	 * Add the enclosure to the schema based on its type.
	 *
	 * @param Frontend_Controller $ss_podcasting
	 * @param string              $enclosure
	 * @param array               $schema
	 *
	 * @return array
	 */
	private function add_enclosure_to_schema( $ss_podcasting, $enclosure, $schema ) {
		$type = $ss_podcasting->get_episode_type();

		$object = array(
			"contentUrl"  => $enclosure,
			"contentSize" => $ss_podcasting->episode_repository->get_file_size(),
		);

		if ( $type === 'audio' ) {
			$object['@type'] = "AudioObject";
			$schema['audio'] = $object;

			return $schema;
		}

		if ( $type === 'video' ) {
			$object['@type'] = "VideoObject";
			$schema['video'] = $object;

			return $schema;
		}

		$object['@type']           = "MediaObject";
		$schema['associatedMedia'] = $object;

		return $schema;
	}
}

MarcusWegener avatar Feb 11 '23 21:02 MarcusWegener