Bulk converting shortcodes to blocks with embeds

Ultimately, I solved the problem by exporting the wp_posts table from the database and doing a regex search along the lines of

'([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]', '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9])', '(.*?)\[soundcloud url=\\"http://api\.soundcloud\.com/tracks/(.*?)\\" params=\\"auto_play=false&show_artwork=false&color=ff7700&show_playcount=false\\" width=\\"100%\\" height=\\"180\\" iframe=\\"true\\" /]

and replacing it with

'\1', '<!-- wp:paragraph -->\2<!-- /wp:paragraph --><!-- wp:embed \{"url":"http://api.soundcloud\.com/tracks/\3","type":"rich","providerNameSlug":"soundcloud","responsive":true\} --> <figure class="wp-block-embed is-type-rich is-provider-soundcloud wp-block-embed-soundcloud"><div class="wp-block-embed__wrapper"> http://api\.soundcloud\.com/tracks/\3 </div></figure> <!-- /wp:embed -->

Although this successfully converts each entire post (more than 5000) to two Gutenberg blocks, WordPress believes the paragraph block is damaged and needs to be recovered. To solve this, on a Github comment I found Javascript code for the edit admin page that will silently recover all supposedly damaged blocks when a post is opened for editing. I am including the code here (credit to AjXUthaya) in case the comment disappears.

admin.js:

import autoRecoverBlocks from './wordpress/autoRecoverBlocks';

// DECONSTRUCT: WP
const { wp = {} } = window || {};
const { domReady, data } = wp;

// AWAIT: jQuery to get ready
jQuery(document).ready(function ($) {
  // DEFINE: Validation variables
  const hasGutenbergClasses = $('body').hasClass('post-php') === true && $('.block-editor').length >= 1;
  const gutenbergHasObject = domReady !== undefined && data !== undefined;
  const gutenbergIsPresent = hasGutenbergClasses === true && gutenbergHasObject === true;

  // IF: Gutenberg editor is present
  if (gutenbergIsPresent === true) {
    autoRecoverBlocks(false);
  }
});

autoRecoverBlocks.js:

// FUNCTION: Recover block
const recoverBlock = (block = null, autoSave = false) => {
  // DECONSTRUCT: WP object
  const { wp = {} } = window || {};
  const { data = {}, blocks = {} } = wp;
  const { dispatch, select } = data;
  const { createBlock } = blocks;
  const { replaceBlock } = dispatch('core/block-editor');
  const wpRecoverBlock = ({ name="", attributes = {}, innerBlocks = [] }) => createBlock(name, attributes, innerBlocks);

  // DEFINE: Validation variables
  const blockIsValid = block !== null
    && typeof block === 'object'
    && block.clientId !== null
    && typeof block.clientId === 'string';

  // IF: Block is not valid
  if (blockIsValid !== true) {
    return false;
  }

  // GET: Block based on ID, to make sure it exists
  const currentBlock = select('core/block-editor').getBlock(block.clientId);

  // IF: Block was found
  if (!currentBlock !== true) {
    // DECONSTRUCT: Block
    const {
      clientId: blockId = '',
      isValid: blockIsValid = true,
      innerBlocks: blockInnerBlocks = [],
    } = currentBlock;

    // DEFINE: Validation variables
    const blockInnerBlocksHasLength = blockInnerBlocks !== null
      && Array.isArray(blockInnerBlocks)
      && blockInnerBlocks.length >= 1;

    // IF: Block is not valid
    if (blockIsValid !== true) {
      // DEFINE: New recovered block
      const recoveredBlock = wpRecoverBlock(currentBlock);

      // REPLACE: Broke block
      replaceBlock(blockId, recoveredBlock);

      // IF: Auto save post
      if (autoSave === true) {
        wp.data.dispatch("core/editor").savePost();
      }
    }

    // IF: Inner blocks has length
    if (blockInnerBlocksHasLength) {
      blockInnerBlocks.forEach((innerBlock = {}) => {
        recoverBlock(innerBlock, autoSave);
      })
    }
  }

  // RETURN
  return false;
};

// FUNCTION: Attempt to recover broken blocks
const autoRecoverBlocks = (autoSave = false) => {
  // DECONSTRUCT: WP object
  const { wp = {} } = window || {};
  const { domReady, data = {} } = wp;
  const { select } = data;

  // AWAIT: For dom to get ready
  domReady(function () {
    setTimeout(
      function () {
        // DEFINE: Basic variables
        const blocksArray = select('core/block-editor').getBlocks();
        const blocksArrayHasLength = Array.isArray(blocksArray)
          && blocksArray.length >= 1;

        // IF: Blocks array has length
        if (blocksArrayHasLength === true) {
          blocksArray.forEach((element = {}) => {
            recoverBlock(element, autoSave);
          });
        }
      },
      1
    )
  });
}

// EXPORT
export default autoRecoverBlocks;

To use the Javascript, save the two sections as two files. Put them in a /js/ directory in your activated template’s directory on your server. Then add an enqueuing function in your template’s functions.php file. My code is straightforward:

function add_script_to_menu_page()
{
    // $pagenow, is a global variable referring to the filename of the current page, 
    // such as ‘admin.php’, ‘post-new.php’
    global $pagenow;
 
    if ($pagenow != 'post.php') {
        return;
    }
     
    // loading js
    wp_register_script( 'autoRecoverBlocks', get_template_directory_uri().'/js/autoRecoverBlocks.js', array('jquery-core'), false, true );
    wp_enqueue_script( 'autoRecoverBlocks' );
}
 
add_action( 'admin_enqueue_scripts', 'add_script_to_menu_page' );

Additionally, for pages that do not have broken blocks but for which I do want to have the classic blocks converted to Gutenberg blocks, I have installed the Convert to Blocks plugin, which silently does the conversion when a post is saved.