How to Migrate from Video Embed WYSIWYG to Drupal Core Media

Image
Vintage film cameras migrating for the winter

Introduction

Many Drupal 8 sites were created before Media was stable in core. At that time, the contributed Video Embed Field project was the de facto standard for embedding YouTube and other remote videos in a Drupal site. These days, though, Drupal's Media handling is stable and the Video Embed Field has long been deprecated. Unfortunately, there are still over 80k installs of Video Embed Field in the wild. The goal of this post is to help you migrate away from Video Embed Field and into Core Media. Specifically, we'll focus on the Video Embed WYSIWYG sub-module.

Clarification: Video Embed Field, Video Embed Media, and Video Embed WYSIWYG

There are three primary modules in the Video Embed Field project: Video Embed Field, Video Embed Media, and Video Embed WYSIWYG. Luckily, there are good docs and tools out there for migrating away from Video Embed Field and Video Embed Media. However, note that the docs are conspicuously silent when it comes to the third module: Video Embed WYSIWYG. This blog post will help fill that gap in documentation.

As an additional clarification, please note that the word "migration" is being used in a somewhat general sense here and has no relationship to the Drupal Migrate API. (Though we at Horizontal indeed have some tips about the Migrate API.)

Prerequisites

The hook examined in this blog post assumes you have done a bit of configuration to get your site ready to handle remote video as core Media. The essential pre-migration steps are:

  1. Enable the core Media module.
  2. Create a Media type for remote video called remote_video with a source field called field_media_oembed_video. (If you used a standard install, this media type should get created automatically. Otherwise, you'll have to create it yourself.)
  3. Configure your text formats to use the core Media Embed button and filter. You're probably using ckeditor4 if you're still using Video Embed WYSIWYG, so these docs should help.

Caveat

There's also one caveat: the code below won't work quite right for formatted text fields that have a cardinality other than 1. In my experience, though, almost all formatted text fields (like body) have a cardinality of 1.

Now onto the code!

The Essential Transformation

Ideally, you don't need to understand this section, but it will help if you end up having to tinker with the code below. Let's look at what we're trying to accomplish at a low level.

A video embedded using Video Embed WYSIWYG uses a json token that looks something like this:

{"preview_thumbnail":"[url to an image]","video_url":"[url to the video]","settings":{"responsive":1,"width":"854","height":"480","autoplay":0},"settings_summary":["Embedded Video (Responsive)."]}

We want to transform that into a <drupal-media> tag with the appropriate attributes.

<drupal-media data-align="center" data-entity-type="media" data-entity-uuid="[media uuid]"></drupal-media>

In order to perform that transformation, we need to create a Media entity based on the json token and use its uuid when creating the <drupal-media> tag. That's all the update hook does, nested within a few loops such that we find and update every video on your site.

Video Embed WYSIWYG Update Hook

When I did this work for a client, I put the following code in the install hook of a custom, but it could just as easily go in an update hook in one of your custom modules. It's totally up to you! Copy and paste and edit as you see fit. And good luck!

function my_module_install() {
  // Find all formatted text fields on all content entities (Node, Paragraph, etc).
  $text_long = \Drupal::service('entity_type.manager')->getStorage('field_storage_config')->loadByProperties(['type' => 'text_long']);
  $text_with_summary = \Drupal::service('entity_type.manager')->getStorage('field_storage_config')->loadByProperties(['type' => 'text_with_summary']);
  $fields = array_merge($text_long, $text_with_summary);
  // Iterate through each formatted text field.
  foreach ($fields as $field) {
    // Find all entities (e.g. Nodes) that have a video embedded in this field.
    $ids = \Drupal::entityTypeManager()
      ->getStorage($field->getTargetEntityTypeId())
      ->getQuery()
      ->condition($field->getName(), '%video_url%', 'LIKE')
      ->accessCheck(FALSE)
      ->execute();
    // Iterate through the entities with embedded videos in this field.
    foreach ($ids as $id) {
      // Attempt to load the entity.
      $entity = \Drupal::entityTypeManager()->getStorage($field->getTargetEntityTypeId())->load($id);
      if ($entity) {
        // Find the json representing the embedded videos using a regex match.
        // The regex pattern is taken from the Video Embed WYSIWYG filter plugin.
        // @see https://git.drupalcode.org/project/video_embed_field/-/blob/8.x-2.x/modules/video_embed_wysiwyg/src/Plugin/Filter/VideoEmbedWysiwyg.php#L133
        $matches = [];
        $text = $entity->{$field->getName()}->value;
        if (!preg_match_all('/(<p>)?(?<json>{(?=.*preview_thumbnail\b)(?=.*settings\b)(?=.*video_url\b)(?=.*settings_summary)(.*)})(<\/p>)?/', $text, $matches)) {
          continue;
        }
        // Iterate through each of the matches. It's possible for a given
        // entity to have more than one video embedded in a given field.
        foreach ($matches['json'] as $delta => $match) {
          // Ensure the JSON string is valid and decode it.
          $embed_data = json_decode($match, TRUE);
          if (!$embed_data || !is_array($embed_data)) {
            continue;
          }
          // Extract the embedded video url from the decoded json data.
          $video_url = $embed_data['video_url'];
          if (empty($video_url)) {
            continue;
          }
          // Depending on the form of $video_url, you may have to do additional
          // processing. The following forms for YouTube urls work well:
          // * https://youtu.be/ABCDEFG1234
          // * https://www.youtube.com/watch?v=ABCDEFG1234
          //
          // However, some other formats are not allowed by Drupal Media such as:
          // x https://www.youtube.com/embed/ABCDEFG1234
          //
          // Just in case, convert from YouTube "embed" url to a "watch" url.
          // You may have to add more custom processing depending on your source.
          $video_url = str_replace('youtube.com/embed/', 'youtube.com/watch?v=',$video_url);
          // Create and save media. (See the pre-requisites.)
          $media = \Drupal::entityTypeManager()->getStorage('media')->create([
            'bundle' => 'remote_video',
            'field_media_oembed_video' => $video_url,
          ]);
          try {
            $media->save();
          }
          catch (Drupal\Core\Entity\EntityStorageException $e) {
            \Drupal::logger('video_embed_wysiwyg_update')->error($e->getMessage());
            continue;
          }
          // Build the media embed code. Note that we align everything in the center for simplicity.
          // If your case is more demanding you can find additional properties on $embed_data.
          $embed = "<drupal-media data-align=\"center\" data-entity-type=\"media\" data-entity-uuid=\"{$media->uuid()}\"></drupal-media>";
          // Replace $match with the embed code in $text
          $text = str_replace($match, $embed, $text);
          // Save the entity with the updated field value.
          $entity->{$field->getName()}->setValue([
            'value' => $text,
            'format' => $entity->{$field->getName()}->format,
            'summary' => $entity->{$field->getName()}->summary,
          ]);
          $entity->save();
        }
      }
    }
  }
}