<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Zac on Medium]]></title>
        <description><![CDATA[Stories by Zac on Medium]]></description>
        <link>https://medium.com/@zac.bostick?source=rss-d942f9d04cde------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Stories by Zac on Medium</title>
            <link>https://medium.com/@zac.bostick?source=rss-d942f9d04cde------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 02 Jun 2026 13:48:38 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@zac.bostick/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Bypassing YouTube API Restrictions]]></title>
            <link>https://medium.com/@zac.bostick/private-youtube-api-fetcher-3459eb4e70b3?source=rss-d942f9d04cde------2</link>
            <guid isPermaLink="false">https://medium.com/p/3459eb4e70b3</guid>
            <category><![CDATA[data-management]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[oauth]]></category>
            <category><![CDATA[nodejs]]></category>
            <dc:creator><![CDATA[Zac]]></dc:creator>
            <pubDate>Thu, 06 Jul 2023 19:35:41 GMT</pubDate>
            <atom:updated>2023-07-08T13:03:18.790Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/979/1*IGXlPVoMfS8BT50S4KggPQ.png" /></figure><p>Recently, while building a mobile app for my organization’s membership portal, I encountered an issue with YouTube API restrictions on private and unlisted videos. These restrictions prevent fetching of unlisted playlists, a significant issue if your organization uses YouTube to store its video content library. To circumvent these limitations, I developed a tool named PrivateYouTubeSync.</p><p>PrivateYouTubeSync is a Node.js application that enables us to fetch, save, and organize data from our organization’s YouTube channel, including private and unlisted videos.</p><p>In this post, let’s delve into the codebase and explore how it works.</p><p>Firstly, we need to import the necessary packages and define some constants. We will use Express.js for handling server-related tasks, Google’s APIs for YouTube data fetching, and fs and path for various file system and path-related operations. We also import json2csv for parsing json and converting it to csv, and our credentials.json file that we will create later.</p><pre>const express = require(&#39;express&#39;);<br>const fs = require(&#39;fs&#39;);<br>const https = require(&#39;https&#39;);<br>const path = require(&#39;path&#39;);<br>const { google } = require(&#39;googleapis&#39;);<br>const { Parser } = require(&#39;json2csv&#39;);<br>const credentials = require(&#39;./credentials.json&#39;);</pre><p>Then, we create our express application and define our port:</p><pre>const app = express();<br>const port = 3000;<br>app.listen(port, () =&gt; {<br>  console.log(`Server is running on port ${port}`);<br>});</pre><p>For OAuth2 authentication, we generate an authorization URL:</p><pre>const { client_id, client_secret, redirect_uris } = credentials.installed;<br>const oauth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);<br>const scopes = [&#39;https://www.googleapis.com/auth/youtube.readonly&#39;];<br>const authUrl = oauth2Client.generateAuthUrl({ access_type: &#39;offline&#39;, scope: scopes });</pre><p>The credentials.json file should look like this:</p><pre>{<br>    &quot;installed&quot;: {<br>        &quot;client_id&quot;: &quot;your-client-id-here.apps.googleusercontent.com&quot;,<br>        &quot;project_id&quot;: &quot;your-project-id-here&quot;,<br>        &quot;auth_uri&quot;: &quot;https://accounts.google.com/o/oauth2/auth&quot;,<br>        &quot;token_uri&quot;: &quot;https://oauth2.googleapis.com/token&quot;,<br>        &quot;auth_provider_x509_cert_url&quot;: &quot;https://www.googleapis.com/oauth2/v1/certs&quot;,<br>        &quot;client_secret&quot;: &quot;your-client-secret-here&quot;,<br>        &quot;redirect_uris&quot;: [&quot;http://localhost:3000/oauth2callback&quot;]<br>    }<br>}</pre><p>To get the above information, you need to set up a new Google Cloud project, enable the YouTube Data API, navigate to the Credentials page in your project, and press “+ CREATE CREDENTIALS” at the top of the page, followed by clicking on “OAuth client ID.” Follow the process and copy-paste your credentials to the proper fields listed above in your credentials.json file.</p><p>Next, we define our app.get() function routes:</p><pre>app.get(&#39;/&#39;, (req, res) =&gt; {<br>  res.redirect(authUrl);<br>});</pre><p>The code above takes you to the OAuth screen to grant access to the related Google/YouTube account you are fetching the videos and playlists from.</p><p>Next, we define our /oauth2callback route, which is where the bulk of our application logic resides:</p><pre>app.get(&#39;/oauth2callback&#39;, (req, res) =&gt; {<br>  const code = req.query.code;<br>  oauth2Client.getToken(code, async (err, tokens) =&gt; {<br>    if (err) {<br>      console.error(&#39;Error retrieving access token:&#39;, err);<br>      return;<br>    }<br><br>    oauth2Client.setCredentials(tokens);<br>    console.log(&#39;Successfully retrieved tokens&#39;);<br><br>    const youtube = google.youtube({<br>      version: &#39;v3&#39;,<br>      auth: oauth2Client,<br>    });<br><br>    async function fetchAllPlaylists(pageToken) {<br>      const response = await youtube.playlists.list({<br>        part: &#39;snippet,contentDetails&#39;,<br>        mine: true,<br>        pageToken: pageToken  <br>      });<br><br>      let playlists = response.data.items;<br><br>      if (response.data.nextPageToken) {<br>        const nextPagePlaylists = await fetchAllPlaylists(response.data.nextPageToken);<br>        playlists = playlists.concat(nextPagePlaylists);<br>      }<br>      if (playlists.length &gt; 1) {<br>        playlists.pop();<br>      }<br>      return playlists;<br>    }<br><br>    const playlists = await fetchAllPlaylists();<br>    console.log(&#39;Successfully retrieved playlists&#39;);<br>      <br>    for (let playlist of playlists) {<br>      const playlistItemsResponse = await youtube.playlistItems.list({<br>        playlistId: playlist.id,<br>        part: &#39;snippet,contentDetails&#39;<br>      });<br>      playlist.videos = playlistItemsResponse.data.items;<br>    }<br><br>    const dataToSave = playlists.map(playlist =&gt; {<br>      return {<br>        id: playlist.id,<br>        title: playlist.snippet.title,<br>        videos: playlist.videos<br>      };<br>    });<br><br>    fs.writeFileSync(&#39;playlists_with_titles_and_videos.json&#39;, JSON.stringify(dataToSave, null, 2));<br>    console.log(&#39;Playlists, their titles and videos saved to playlists_with_titles_and_videos.json&#39;);<br><br>    await convertJsonToCsv();<br><br>    fs.mkdir(path.join(__dirname, &#39;thumbnails&#39;), { recursive: true }, (err) =&gt; {<br>      if (err) throw err;<br>    });<br><br>    dataToSave.forEach((playlist) =&gt; {<br>      playlist.videos.forEach((video) =&gt; {<br>        if (video.snippet.thumbnails &amp;&amp; video.snippet.thumbnails.maxres) {<br>          const imageUrl = video.snippet.thumbnails.maxres.url;<br>          const videoId = video.snippet.resourceId.videoId;<br>          const imagePath = path.join(__dirname, &#39;thumbnails&#39;, `${videoId}.jpg`);<br><br>          downloadImage(imageUrl, imagePath, (err) =&gt; {<br>            if (err) throw err;<br>            console.log(`Downloaded image for video ${videoId}`);<br>          });<br>        } else {<br>          console.log(`No high resolution thumbnail available for video ${video.snippet.resourceId.videoId}`);<br>        }<br>      });<br>    });<br><br>    res.send(&#39;Playlists, their titles and videos saved. Images are downloading.&#39;); <br>  });<br>});</pre><p>This code handles the YouTube API permission request and retrieves a JSON filecontaining all our playlists in the project folder. It also creates a folder called ‘thumbnails’ to download all of the maximum resolution thumbnails and store them with their relative video ID in jpg format. This is accomplished via our downloadImage() function:</p><pre>function downloadImage(url, dest, cb) {<br>  const file = fs.createWriteStream(dest);<br>  https.get(url, (response) =&gt; {<br>    response.pipe(file);<br>    file.on(&#39;finish&#39;, () =&gt; {<br>      file.close(cb);<br>    });<br>  });<br>}</pre><p>Lastly, we need to define our convertJsonToCsv() function, which is responsible for transforming our JSON data into CSV format:</p><pre>async function convertJsonToCsv() {<br>  const playlists = require(&#39;./playlists_with_titles_and_videos.json&#39;);<br><br>  const flattenedData = playlists.reduce((arr, playlist) =&gt; {<br>    playlist.videos.forEach(video =&gt; {<br>      const thumbnailUrl = video.snippet.thumbnails?.maxres?.url || video.snippet.thumbnails?.high?.url || video.snippet.thumbnails?.default?.url;<br><br>      arr.push({<br>        playlistId: playlist.id,<br>        playlistTitle: playlist.title,<br>        videoId: video.contentDetails.videoId,<br>        videoTitle: video.snippet.title,<br>        videoDescription: video.snippet.description,<br>        videoThumbnailUrl: thumbnailUrl,<br>        videoPublishedAt: video.snippet.publishedAt<br>      });<br>    });<br>    return arr;<br>  }, []);<br><br>  const parser = new Parser();<br>  const csv = parser.parse(flattenedData);<br><br>  fs.writeFileSync(&#39;playlists_with_videos.csv&#39;, csv);<br>  console.log(&#39;CSV conversion complete&#39;);<br>}</pre><p>This function flattens the data structure, ensuring each video has its own record, and then uses the json2csv library to convert the data to CSV. Once complete, the function saves the CSV data to a file named &#39;playlists_with_videos.csv&#39;.</p><p>In conclusion, PrivateYouTubeSync is a handy tool that efficiently tackles the restrictions imposed by the YouTube API on private and unlisted videos. The tool’s ability to fetch, save, and organize data, including private and unlisted videos, is a significant relief for organizations that heavily rely on YouTube for their video content management.</p><p>Feel free to dive into the codebase and explore the tool further. You can access/clone the project on <a href="https://github.com/zac-bostick/PrivateYoutubeSync">GitHub</a>.</p><p>If you have any questions, feedback, or if you want to share your experience using this tool, I would be thrilled to hear from you. Feel free to connect with me on <a href="https://twitter.com/zacdbostick">Twitter</a>, <a href="https://www.instagram.com/zacdbostick">Instagram</a> or Threads, or visit my <a href="https://zacbostick.com">Website</a>.</p><p>Happy coding!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3459eb4e70b3" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>