<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Embedfinder]]></title><description><![CDATA[Embedfinder]]></description><link>https://embedfinder.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ae118f6e903d04545634126/39c6e0c1-5dce-4023-801d-7a40255ce686.png</url><title>Embedfinder</title><link>https://embedfinder.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 31 Oct 2026 10:52:30 GMT</lastBuildDate><atom:link href="https://embedfinder.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to import a YouTube playlist into WordPress with titles, thumbnails and embeds]]></title><description><![CDATA[Turn a YouTube playlist into WordPress drafts with title, thumbnail and player: a free PHP script with the YouTube Data API, and a shortcut.]]></description><link>https://embedfinder.hashnode.dev/how-to-import-a-youtube-playlist-into-wordpress-with-titles-thumbnails-and-embeds</link><guid isPermaLink="true">https://embedfinder.hashnode.dev/how-to-import-a-youtube-playlist-into-wordpress-with-titles-thumbnails-and-embeds</guid><category><![CDATA[WordPress]]></category><category><![CDATA[youtube]]></category><category><![CDATA[PHP]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Juan F Sanc]]></dc:creator><pubDate>Tue, 27 Oct 2026 22:01:14 GMT</pubDate><content:encoded><![CDATA[<p>You have a YouTube playlist with 40 videos and you want 40 WordPress posts: each
one with the video title, the description, the thumbnail as featured image and
the player embedded. Copying that by hand takes an evening. Here are two ways to
do it: a free one with the YouTube Data API and a PHP script, and a shorter one
with a hosted tool.</p>
<p><em>Disclosure: we build Embedfinder, the tool in part 2. Part 1 works without it.</em></p>
<h3>Part 1: do it yourself with the YouTube Data API</h3>
<p><strong>What you need:</strong></p>
<ul>
<li>A YouTube Data API v3 key (free in Google Cloud Console → APIs &amp; Services → Credentials). Reading a playlist page and a batch of 50 videos costs 1 quota unit each; the default quota is 10,000 units a day, so a playlist of a few hundred videos is fine.</li>
<li>WP-CLI on your server, to run the script with <code>wp eval-file</code>.</li>
<li>The playlist ID: the <code>list=</code> part of <code>https://www.youtube.com/playlist?list=PL...</code>.</li>
</ul>
<p><strong>Step 1: get every video ID in the playlist.</strong> <code>playlistItems.list</code> returns 50 items per page and a <code>nextPageToken</code> while there are more.</p>
<p><strong>Step 2: get the details in batches of 50.</strong> <code>playlistItems</code> doesn't include the duration or the full tags, so ask <code>videos.list</code> with up to 50 IDs at once.</p>
<p><strong>Step 3: create a draft per video.</strong> If the post content has the YouTube URL on its own line, WordPress turns it into a player automatically (oEmbed). No iframe needed.</p>
<p><strong>Step 4: set the featured image and skip duplicates.</strong> <code>media_sideload_image()</code> downloads the thumbnail into the media library; we store the source URL in a custom field and check it before inserting.</p>
<p>Save this as <code>import-playlist.php</code>:</p>
<pre><code class="language-php">&lt;?php
// Run with: wp eval-file import-playlist.php
$key      = 'YOUR_API_KEY';
$playlist = 'PL_YOUR_PLAYLIST_ID';

require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';

function yt( $endpoint, $args ) {
    $url = add_query_arg( array_filter( $args ), "https://www.googleapis.com/youtube/v3/$endpoint" );
    $res = wp_remote_get( $url, [ 'timeout' =&gt; 20 ] );
    if ( is_wp_error( $res ) ) WP_CLI::error( $res-&gt;get_error_message() );
    return json_decode( wp_remote_retrieve_body( $res ), true );
}

// 1. All video IDs, 50 per page
$ids = []; $token = '';
do {
    $page  = yt( 'playlistItems', [ 'part' =&gt; 'contentDetails', 'maxResults' =&gt; 50,
             'playlistId' =&gt; $playlist, 'pageToken' =&gt; $token, 'key' =&gt; $key ] );
    foreach ( $page['items'] ?? [] as $item ) $ids[] = $item['contentDetails']['videoId'];
    $token = $page['nextPageToken'] ?? '';
} while ( $token );

// 2 and 3. Details in batches of 50, one draft per video
foreach ( array_chunk( $ids, 50 ) as $batch ) {
    $data = yt( 'videos', [ 'part' =&gt; 'snippet,contentDetails', 'id' =&gt; implode( ',', $batch ), 'key' =&gt; $key ] );
    foreach ( $data['items'] ?? [] as $v ) {
        $url = 'https://www.youtube.com/watch?v=' . $v['id'];
        if ( get_posts( [ 'post_type' =&gt; 'post', 'post_status' =&gt; 'any', 'fields' =&gt; 'ids',
             'meta_key' =&gt; '_source_url', 'meta_value' =&gt; $url ] ) ) continue; // already imported

        $s       = $v['snippet'];
        $post_id = wp_insert_post( [
            'post_title'   =&gt; $s['title'],
            'post_content' =&gt; $url . "\n\n" . $s['description'],
            'post_status'  =&gt; 'draft',
            'tags_input'   =&gt; array_slice( $s['tags'] ?? [], 0, 5 ),
        ] );

        // 4. Thumbnail as featured image (maxres doesn't exist for every video)
        $thumb = $s['thumbnails']['maxres']['url'] ?? $s['thumbnails']['high']['url'];
        $img   = media_sideload_image( $thumb, $post_id, $s['title'], 'id' );
        if ( ! is_wp_error( $img ) ) set_post_thumbnail( $post_id, $img );

        // Duration comes as ISO 8601 (PT4M13S); store it in seconds
        $d = new DateInterval( $v['contentDetails']['duration'] );
        update_post_meta( $post_id, 'duration', $d-&gt;d * 86400 + $d-&gt;h * 3600 + $d-&gt;i * 60 + $d-&gt;s );
        update_post_meta( $post_id, '_source_url', $url );
        WP_CLI::log( "Draft $post_id: {$s['title']}" );
    }
}
</code></pre>
<p>Things that will bite you:</p>
<ul>
<li><strong>Private and deleted videos</strong> still appear in <code>playlistItems</code> but not in <code>videos.list</code>, so they are skipped on their own.</li>
<li><strong><code>maxresdefault.jpg</code> is missing</strong> on many older videos; that's why the fallback to <code>high</code>.</li>
<li><strong>Keep your key server-side.</strong> Restrict it to the YouTube Data API in Cloud Console.</li>
<li><strong>Run it again safely:</strong> the <code>_source_url</code> check means only new videos are added, so you can schedule it with WP-Cron or a system cron to keep the playlist in sync.</li>
</ul>
<p>That's the whole job for YouTube. It gets longer when the videos come from somewhere else: Vimeo has its own oEmbed endpoint, other sites need Open Graph or JSON-LD parsing, and listing pages need pagination.</p>
<h3>Part 2: the same with Embedfinder</h3>
<p><a href="https://embedfinder.com/en?ref=hashnode">Embedfinder</a> reads YouTube through its official API, Vimeo through oEmbed and other pages through Open Graph and JSON-LD, and returns one entry per video: title, description, poster, tags, date, duration and the player embeds. It doesn't download any video.</p>
<p><strong>On the web:</strong> create a free account, paste the playlist URL and you get the list of videos; paste them to get the entries, then copy them or download JSON. The free plan is 10 entries a day and shows the first 10 videos of a listing, enough to check the output. A full playlist and CSV export are in the Basic plan (US$ 7/month, 1,000 entries).</p>
<p><strong>With the API (Premium plan):</strong> first ask for the list, then send the links in groups of 8, the per-request limit:</p>
<pre><code class="language-bash">EF_KEY="ef_your_key"

curl -s -X POST https://embedfinder.com/api/v1/listado \
  -H "Authorization: Bearer $EF_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://www.youtube.com/playlist?list=PL_YOUR_PLAYLIST_ID"}' &gt; list.json

# {"fichas": ["https://www.youtube.com/watch?v=...", ...], "siguiente": "..."}
# 50 videos per page; send "siguiente" again for the next page.

jq -c '.fichas as $u | range(0; $u|length; 8) | {urls: $u[.:.+8]}' list.json |
while read -r body; do
  curl -s -X POST https://embedfinder.com/api/v1/extraer \
    -H "Authorization: Bearer $EF_KEY" -H "Content-Type: application/json" \
    -d "$body" | jq -c '.fichas[]'
  sleep 3   # stay under 20 requests per minute
done &gt; entries.jsonl
</code></pre>
<p>Each line of <code>entries.jsonl</code> is one entry. The field names are in Spanish: <code>titulo</code> (title), <code>descripcion</code>, <code>portada</code> (poster), <code>generos</code> (tags), <code>duracion</code> (seconds) and <code>servidores</code> (players, best first; <code>clase: "embed"</code> is a known embeddable player). In the script from part 1, replace the <code>videos.list</code> loop with:</p>
<pre><code class="language-php">foreach ( file( 'entries.jsonl' ) as $line ) {
    $f = json_decode( $line, true );
    // same wp_insert_post / media_sideload_image as above, with
    // $f['titulo'], $f['descripcion'], $f['portada'], $f['generos'],
    // $f['duracion'] and $f['servidores'][0]['url'] as the player
}
</code></pre>
<p><strong>Without code:</strong> the Premium plan includes a WordPress plugin. You install it, paste its key in your Embedfinder account, and "Send to…" creates the drafts with featured image, tags and player. Every account gets 20 free sends to try it.</p>
<p>The same calls work for a Vimeo video or any page with a video player, which is where the do-it-yourself script would need a new parser per site.</p>
<p>If you try either version and something comes out wrong, tell us in the comments.</p>
]]></content:encoded></item></channel></rss>