Cómo construir un bloque de videos en YouTube usando el RSS

04 February 2021
0 Comentarios
Youtube RSS feed Drupal

Youtube es de esas plataformas esenciales para toda estrategia de mercadeo digital que se realice hoy en día, la cantidad de personas que constantemente consumen material desde allí es abrumadora y las marcas siempre van a querer aprovechar esos canales, es por eso que es común que además quieran mostrar en los sitios web los videos que han publicado en sus canales para enganchar a sus usuarios con el contenido que han creado.

En términos de desarrollo, la forma recomendada y práctica de obtener información sobre un canal de youtube a fin de armar un bloque con los mas recientes videos subidos es recurriendo a la API, esta API está sobre la nube de servicios de Google en Google Cloud y el procedimiento aunque es sencillo de comprender para un desarrollador, no lo es para un usuario que por lo regular solo publica contenidos, miremos los pasos que indica Google en su sitio web.

Blog-youtube

Note que para usar el API hay que tener una cuenta Google Cloud y por lo regular eso también puede implicar tener que registrar una tarjeta de crédito, no es algo tan difícil si esto lo hace un desarrollador y solo una vez.

Definición del requerimiento

En un proyecto nos encontramos con un requerimiento más particular, una instalación multidominio separado en multisitios mediante group requería que en la configuración de cada misitio, el webmaster o administrador del sitio pudiese configurar su canal de youtube para mostrar los últimos videos publicados, el problema era que estos administradores tenían muy poca o nula formación técnica como para seguir con éxito los pasos para registrar un API Key de consulta ante google y adicionalmente esa sería una tarea para cerca de 300 minisitios. Necesitábamos encontrar una solución práctica para este requerimiento.

Caso de solución

Investigando un poco encontramos que Youtube aún mantiene funcionando los canales RSS para cada canal de tal forma que gracias a la estructura XML que tiene los RSS podemos hacer uso de un intérprete de RSS para extraer los datos de los últimos videos y nos ahorra todo el proceso de registro en Google Cloud para obtener la información.

Hoy en día el RSS no es tan popular pero sigue siendo un canal de alimentación abierta usado por quienes aún gustan de los agregadores y en este caso en particular nos facilita crear un pequeño agregador RSS que visualice en Drupal los últimos videos subidos al canal.

Para la solución de este caso en Drupal decidimos crear un formateador para un campo de texto que recibe el ID del canal, este ID se obtiene de la URL del canal

Imagen blog

El campo creado se visualiza así:

Blog-youtube

Cuando el usuario ingrese el ID del Canal lo usamos para armar la URL del RSS y leemos la estructura para extraer la información de los últimos tres videos subidos.

Para la lectura de la estructura HTML recurrimos a la librería Láminas Feed Laminas\Feed que ya viene entre los paquetes base de cualquier instalación de Drupal 8 y 9, de  acuerdo a la descripción de la librería:

“Láminas \ Feed proporciona funcionalidad para consumir feeds RSS y Atom. Proporciona una sintaxis natural para acceder a elementos de feeds, atributos de feed y atributos de entrada.”

A pesar que Láminas ya nos ofrece una forma de leer una estructura RSS, la estructura de youtube tiene algunas estructuras que no hacen parte de un RSS estándar como lo podemos observar en la siguiente imagen.

Blog-youtube

Para poder indicarle a Laminas como leer esta estructura de datos, debemos crearle una extensión que podemos incluir dentro del Módulo Custom que estemos creando para nuestra solución, creamos un archivo Entry.php en la estructura de código que puede ver en la siguiente imagen.

Blog-youtube

En este archivo hacemos lo siguiente:

  • Creamos una clase llamada Entry en el espacio de Nombre de la extensión YoutubeRSS, esta clase extiende de AbstractEntry
  • Creamos cuatro métodos para definir cómo leer cada campo, los métodos son:
    • getMediaThumbnail
    • getMediaContent
    • getMediaDescription
    • getMediaTitle
  • Registramos un espacio de nombre llamado youtube usando el método registerNamespaces

Observe en el código cómo se realizó:

<?php

namespace Drupal\my_module\Extension\YoutubeRss;

use Laminas\Feed\Reader\Extension\AbstractEntry;

class Entry extends AbstractEntry
{
    public function getMediaThumbnail()
    {
        if (isset($this->data['media_thumbnail'])) {
            return $this->data['media_thumbnail'];
        }

        $media_thumbnail = $this->xpath->evaluate(
            'string(' . $this->getXpathPrefix() . '/media:group/media:thumbnail[@url]/@url)'
        );

        if (! $media_thumbnail) {
            $media_thumbnail = null;
        }

        $this->data['media_thumbnail'] = $media_thumbnail;
        return $this->data['media_thumbnail'];
    }

    public function getMediaContent()
    {
        if (isset($this->data['media_content'])) {
            return $this->data['media_content'];
        }

        $media_content = $this->xpath->evaluate(
            'string(' . $this->getXpathPrefix() . '/media:group/media:content[@url]/@url)'
        );

        if (! $media_content) {
            $media_content = null;
        }

        $this->data['media_content'] = $media_content;
        return $this->data['media_content'];
    }

    public function getMediaDescription()
    {
        if (isset($this->data['media_description'])) {
            return $this->data['media_description'];
        }

        $media_description = $this->xpath->evaluate(
            'string(' . $this->getXpathPrefix() . '/media:group/media:description)'
        );

        if (! $media_description) {
            $media_description = null;
        }

        $this->data['media_description'] = $media_description;
        return $this->data['media_description'];
    }

    public function getMediaTitle()
    {
        if (isset($this->data['media_title'])) {
            return $this->data['media_title'];
        }

        $media_title = $this->xpath->evaluate(
            'string(' . $this->getXpathPrefix() . '/media:group/media:title)'
        );

        if (! $media_title) {
            $media_title = null;
        }

        $this->data['media_title'] = $media_title;
        return $this->data['media_title'];
    }

    protected function registerNamespaces()
    {
        $this->xpath->registerNamespace(
            'youtube',
            'https://www.youtube.com/feeds/videos.xml'
        );
    }
}

Ya que hemos creado los métodos necesarios para indicar cómo leer esas estructuras, podemos crear el formateador para el campo y hacer uso de láminas para leer el RSS y procesar la estructura según sea necesario.

Creamos un Archivo PHP llamado YoutubeRSSParser.php que puede ver como queda en la estructura de carpetas de la siguiente imagen:

Blog-youtube

En el archivo realizamos lo siguiente:

  • Se crea una clase llamada YoutubeRSSParser que extiende de FormatterBase, en las anotaciones se definen las propiedades id, label y field_types, este último indicando que es un formateador solo para campos tipo string.
  • El método viewValue invoca el método RSSParser en el cual de invoca la Clase YoutubeRss que habíamos creado y la registra para que sea tenida en cuenta en el procesamiento de la estructura.
  • Se construye la URL con el ID del canal recibido desde el campo y esa URL se pasa por un método llamado process.
  • El método process importa el feed y extrae los datos usando los métodos getTitle, getDescription, getLink y getMediaThumbnail definidos previamente además de otros métodos ya existentes el resultado es almacenado en un arreglo de datos que finalmente es pasado a una plantilla twig para ser estructurados en HTML

El código es el siguiente:

<?php

namespace Drupal\my_module\Plugin\Field\FieldFormatter;

use Drupal\Component\Utility\Html;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\aggregator\FeedInterface;
use Laminas\Feed\Reader\Reader;
use Laminas\Feed\Reader\Exception\ExceptionInterface;
use Drupal\my_module\Extension\YoutubeRss;
use Laminas\Feed\Reader\ExtensionManager;
use Laminas\Feed\Reader\ExtensionPluginManager;


/**
 * Plugin implementation of the 'youtube_rss_parser' formatter.
 *
 * @FieldFormatter(
 *   id = "youtube_rss_parser",
 *   label = @Translation("Youtube RSS Parser"),
 *   field_types = {
 *     "string"
 *   }
 * )
 */
class YoutubeRSSParser extends FormatterBase {

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      // Implement default settings.
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    return [
      // Implement settings form.
    ] + parent::settingsForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $summary = [];
    // Implement settings summary.

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];

    foreach ($items as $delta => $item) {
      $elements[$delta] = ['#markup' => $this->viewValue($item)];
    }

    return $elements;
  }

  /**
   * Generate the output appropriate for one field item.
   *
   * @param \Drupal\Core\Field\FieldItemInterface $item
   *   One field item.
   *
   * @return string
   *   The textual output generated.
   */
  protected function viewValue(FieldItemInterface $item) {
    // The text value has no text format assigned to it, so the user input
    // should equal the output, including newlines.
    return $this->RSSParser($item->value);
  }

  private function RSSParser($value){
    $extensions = new ExtensionPluginManager();
    $extensions->setInvokableClass('YoutubeRss\Entry', YoutubeRss\Entry::class);
    Reader::setExtensionManager(new ExtensionManager($extensions));
    Reader::registerExtension('YoutubeRss');

    $urlFeed = "https://www.youtube.com/feeds/videos.xml?channel_id=".$value;
    return $this->process($urlFeed);
  }

  private function process($feed){
    try {
      $channel = Reader::import($feed);
      $link = $channel->getLink();
      $data = [];
      foreach ($channel as $item) {
        // Reset the parsed item.
        $parsed_item = [];
        // Move the values to an array as expected by processors.
        $parsed_item['title'] = $item->getTitle();
        $parsed_item['description'] = $item->getDescription();
        $parsed_item['link'] = $item->getLink();
        $parsed_item['media_thumbnail'] = $item->getMediaThumbnail();
        // Store on $feed object. This is where processors will look for parsed items.
        $data[] = $parsed_item;
      }

      $build_content = [
        '#theme' => 'youtube_feed_block',
        '#title' => $channel->getTitle(),
        '#items' => $data,
      ];
      $rendered = \Drupal::service('renderer')->renderPlain($build_content);
      return $rendered;
    }
      
    catch (ExceptionInterface $e) {
      watchdog_exception('my_module', $e);

      return FALSE;
    }

  }

}

Como pudimos ver en el anterior código, prácticamente todo el procesamiento fue realizado en unas cuantas líneas de código y su resultado es enviado en un arreglo a una plantilla Twig.

La plantilla fue definida en el archivo .module del módulo es decir en my_module.module en el siguiente código podemos ver como se ha implementado el hook_theme para definir el theme youtube_feed_block quien a su vez define que el nombre del archivo twig a usar es  block--youtube-feed-block

/**
 * Implements hook_theme().
 */
function my_module_theme() {
  return [
    'youtube_feed_block' => [
      'variables' => [
        'title' => NULL,
        'items' => NULL,
      ],
      'template' => 'block--youtube-feed-block',
    ],

  ];
}

Finalmente creamos el archivo Twig para estructurar el HTML resultante el cual se verá al momento de renderizar el campo, el archivo debe ser creado en la carpeta templates del módulo como lo vemos en la siguiente imagen.

Blog-youtube

En nuestro caso el contenido del HTML es el siguiente, notará que lo que hace es recorrer los ítems e imprimirlos en una lista.

<div class="layout--threecol-section--33-34-33 flex">
    <div class="social-media--container">
        <h3 class="title-social-network">YouTube</h3>
    </div>
    <div class="block-youtubechannel-block block-youtube--33">
        <h2 class="panel-heading">{{ title }}</h2>
        <div class="youtube-block-channel">
            <ul>
                {% for item in items %}
                    <li><a href = {{ item.link }} target="_blank" ><img src = {{ item.media_thumbnail }} /></a></li>
                    <li class = "title-video-youtube"><a href = {{ item.link }} target="_blank" class="link-title">{{ item.title }}</a></li>
                {% endfor %}
            </ul>
        </div>
    </div>
</div>

Conclusiones

El método de solución que implementamos es práctico para una solución muy puntual que requiera mostrar los últimos videos publicados en un canal, sin embargo para soluciones más avanzadas si es necesario el uso del API.

Espero que les sea de utilidad para saber como extender la librería láminas, como parsear un RSS con Drupal, crear un nuevo formateador para un campo y renderizar en una plantilla.

 

 

 

 

 

 

Aldibier Morales Morales
Aldibier Morales Morales