{"id":688,"date":"2026-07-17T02:09:16","date_gmt":"2026-07-16T23:09:16","guid":{"rendered":"https:\/\/mexela.com\/blog\/youtube-channel-competitor-analysis-public-data\/"},"modified":"2026-07-22T00:20:46","modified_gmt":"2026-07-21T21:20:46","slug":"youtube-channel-competitor-analysis-public-data","status":"publish","type":"post","link":"https:\/\/mexela.com\/blog\/youtube-channel-competitor-analysis-public-data\/","title":{"rendered":"Build a YouTube Channel Competitor Analysis With Public Data"},"content":{"rendered":"<p><!-- mexela-platform-guide:start --><\/p>\n<p class='mexela-answer'>Build a YouTube channel competitor analysis from a fixed channel list, a fixed 90-day UTC window, and the same official Data API path for every channel: resolve stable channel IDs, read each uploads playlist from <code>contentDetails.relatedPlaylists.uploads<\/code>, page through <code>playlistItems.list<\/code>, then batch the collected video IDs through <code>videos.list<\/code>. Report simple public measures such as upload cadence and median public views, plus the count coverage behind each measure. Do not infer revenue, conversions, demographics, retention, or causal performance.<\/p>\n<p>This example answers one editorial question: how did three comparable networking-education channels publish during 1 April through 29 June 2026? The names and values in the finished report are fictional so they cannot be mistaken for live claims. In a real run, replace them with approved public channels, capture their exact IDs, and freeze the set before requesting any video data.<\/p>\n<h2>Freeze the cohort and 90-day window<\/h2>\n<p>The illustrative cohort is Northstar Network Lab, Cedar Router School, and Meridian Wi-Fi Workshop. All three are fictional tutorial channels aimed at home-networking learners. The analysis window starts <code>2026-04-01T00:00:00Z<\/code> and ends before <code>2026-06-30T00:00:00Z<\/code>, which is exactly 90 days. A video belongs when its API <code>snippet.publishedAt<\/code> falls inside that half-open interval.<\/p>\n<table>\n<thead>\n<tr>\n<th>Frozen label<\/th>\n<th>Required production value<\/th>\n<th>Reason included<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>northstar-network-lab<\/td>\n<td>Verified channel ID beginning <code>UC<\/code><\/td>\n<td>Beginner router walkthroughs<\/td>\n<\/tr>\n<tr>\n<td>cedar-router-school<\/td>\n<td>Verified channel ID beginning <code>UC<\/code><\/td>\n<td>Home Wi-Fi troubleshooting<\/td>\n<\/tr>\n<tr>\n<td>meridian-wifi-workshop<\/td>\n<td>Verified channel ID beginning <code>UC<\/code><\/td>\n<td>Comparable device tutorials<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Do not rediscover the cohort from search on every refresh. Display names and handles can change; the channel ID is the join key. If a stakeholder adds a fourth channel after collection, start a new report version or rerun the same window for all four rather than appending an incomparable row.<\/p>\n<h2>Resolve channels, then read their uploads playlists<\/h2>\n<p>If the input arrives as a handle, call <code>GET https:\/\/www.googleapis.com\/youtube\/v3\/channels?part=id,snippet,contentDetails&amp;forHandle={HANDLE}&amp;key={KEY}<\/code> once for that handle. The <a href='https:\/\/developers.google.com\/youtube\/v3\/docs\/channels\/list' data-source='primary'><code>channels.list<\/code> reference<\/a> documents <code>forHandle<\/code>, the <code>id<\/code> filter, and the <code>contentDetails<\/code> part. Confirm one returned resource, store its ID, and use IDs thereafter.<\/p>\n<p>For the frozen IDs, request <code>part=id,snippet,contentDetails,statistics<\/code> with a comma-separated <code>id<\/code> value. Read the uploads playlist at <code>items[].contentDetails.relatedPlaylists.uploads<\/code>. The method currently costs 1 quota unit per call. Channel-level subscriber counts are context only; they can be rounded, hidden, and accumulated over very different histories, so they are not a denominator for the report below.<\/p>\n<p>This route avoids using <code>search.list<\/code> to rediscover uploads. YouTube represents a channel&#8217;s uploaded videos through its uploads playlist, and the official API&#8217;s <a href='https:\/\/developers.google.com\/youtube\/v3\/sample_requests' data-source='primary'>sample request sequence<\/a> demonstrates retrieving that playlist from the channel resource before listing its items.<\/p>\n<h2>Page every uploads playlist and record completeness<\/h2>\n<p>For each uploads playlist, call <code>GET https:\/\/www.googleapis.com\/youtube\/v3\/playlistItems?part=contentDetails,snippet&amp;playlistId={UPLOADS_ID}&amp;maxResults=50&amp;pageToken={TOKEN}&amp;key={KEY}<\/code>. Omit <code>pageToken<\/code> on the first call, then pass each response&#8217;s <code>nextPageToken<\/code> into the next request until no token remains. The <a href='https:\/\/developers.google.com\/youtube\/v3\/docs\/playlistItems\/list' data-source='primary'><code>playlistItems.list<\/code> reference<\/a> documents a maximum of 50 items per page, token-based pagination, and a cost of 1 quota unit per call.<\/p>\n<p>Filter by the analysis window after collecting the playlist item timestamps. Do not silently stop after one page or assume <code>pageInfo.totalResults<\/code> replaces pagination. For a very large archive, a reviewed page cap can protect the run, but the report must then say <code>playlistComplete: false<\/code> and must not present its window totals as complete.<\/p>\n<p>Deduplicate video IDs across pages, retain the playlist-item published time for diagnostics, and log pages fetched per channel. Deleted or private items may be absent from the final video lookup. Keep that difference as a data-quality count instead of inventing a zero-view record.<\/p>\n<h2>Fetch public video details in batches<\/h2>\n<p>Split the in-window IDs into batches of no more than 50 and call <code>GET https:\/\/www.googleapis.com\/youtube\/v3\/videos?part=snippet,contentDetails,statistics&amp;id={COMMA_SEPARATED_IDS}&amp;key={KEY}<\/code>. The <a href='https:\/\/developers.google.com\/youtube\/v3\/docs\/videos\/list' data-source='primary'><code>videos.list<\/code> reference<\/a> documents the ID filter, response parts, and current 1-unit cost. The separate <a href='https:\/\/developers.google.com\/youtube\/v3\/docs\/videos' data-source='primary'>video resource reference<\/a> defines public <code>statistics.viewCount<\/code> and <code>statistics.commentCount<\/code> and explains the meaning of <code>snippet.publishedAt<\/code>.<\/p>\n<p>Normalize decimal strings from <code>statistics<\/code> into non-negative integers only when the field exists and parses safely. A missing count stays <code>null<\/code>; it is not zero. Keep ISO 8601 duration as returned or parse it with a tested duration library if the report needs minutes. One normalized, fictional record looks like this:<\/p>\n<pre data-check='json'><code class='language-json'>{\n  \"channelKey\": \"northstar-network-lab\",\n  \"channelId\": \"UC_REPLACE_WITH_VERIFIED_ID\",\n  \"videoId\": \"demoVideo042\",\n  \"title\": \"Place a mesh node without creating interference\",\n  \"publishedAt\": \"2026-05-12T14:00:00Z\",\n  \"duration\": \"PT8M14S\",\n  \"viewCount\": 18400,\n  \"commentCount\": 73,\n  \"capturedAt\": \"2026-07-01T08:00:00Z\"\n}<\/code><\/pre>\n<p>Counts are snapshots at <code>capturedAt<\/code>, not values at publication. Comparing lifetime views across videos of different ages remains imperfect even inside one window. Show the publication range and capture time beside the summary so a reader can see that limitation.<\/p>\n<h2>Calculate cadence and median views transparently<\/h2>\n<p>Publishing cadence is the number of in-window uploads divided by window weeks. For 90 days, the denominator is <code>90 \/ 7<\/code>. Median public views sorts only videos with a numeric public view count and takes the middle value, or the average of the two middle values. Always report <code>videosWithViewCount<\/code> beside the median so missing statistics cannot disappear from the denominator.<\/p>\n<pre data-check='node'><code class='language-javascript'>function median(values) {\n  if (values.length === 0) return null;\n  const sorted = [...values].sort((left, right) =&gt; left - right);\n  const middle = Math.floor(sorted.length \/ 2);\n  return sorted.length % 2 === 1\n    ? sorted[middle]\n    : (sorted[middle - 1] + sorted[middle]) \/ 2;\n}\nfunction calculateMetrics(videos, windowDays) {\n  if (!Array.isArray(videos)) throw new TypeError('videos must be an array');\n  if (!Number.isFinite(windowDays) || windowDays &lt;= 0) {\n    throw new RangeError('windowDays must be positive');\n  }\n  const publicViews = videos\n    .map((video) =&gt; video.viewCount)\n    .filter((value) =&gt; Number.isFinite(value) &amp;&amp; value &gt;= 0);\n  return {\n    uploads: videos.length,\n    uploadsPerWeek: Number((videos.length \/ (windowDays \/ 7)).toFixed(2)),\n    medianPublicViews: median(publicViews),\n    videosWithViewCount: publicViews.length,\n  };\n}\nconst channels = [\n  {\n    channelKey: 'northstar-network-lab',\n    videos: [18400, 12100, 9400, 22100, 18700, 16500]\n      .map((viewCount) =&gt; ({ viewCount })),\n  },\n  {\n    channelKey: 'cedar-router-school',\n    videos: [6200, 8300, null, 9700, 10400, 11900, 7300, 15100, 9100]\n      .map((viewCount) =&gt; ({ viewCount })),\n  },\n  {\n    channelKey: 'meridian-wifi-workshop',\n    videos: [26300, 21400, 33800, 29700]\n      .map((viewCount) =&gt; ({ viewCount })),\n  },\n];\nconst report = channels.map(({ channelKey, videos }) =&gt; ({\n  channelKey,\n  ...calculateMetrics(videos, 90),\n}));\nconsole.log(JSON.stringify(report, null, 2));<\/code><\/pre>\n<p>The code counts every published video for cadence but excludes the one missing view count from Cedar&#8217;s median. It does not replace missing data with zero. Run it with Node.js 18 or newer; it has no package dependencies and all fixture values are defined in the example.<\/p>\n<h2>Finish a one-page comparison without a score<\/h2>\n<p>The completed fictional output below includes collection coverage before interpretation. Values are illustrative, not observations about real channels.<\/p>\n<table>\n<thead>\n<tr>\n<th>Channel<\/th>\n<th>Uploads<\/th>\n<th>Uploads\/week<\/th>\n<th>Median public views<\/th>\n<th>View-count coverage<\/th>\n<th>Observed format note<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Northstar Network Lab<\/td>\n<td>6<\/td>\n<td>0.47<\/td>\n<td>17,600<\/td>\n<td>6\/6<\/td>\n<td>Six focused setup walkthroughs<\/td>\n<\/tr>\n<tr>\n<td>Cedar Router School<\/td>\n<td>9<\/td>\n<td>0.70<\/td>\n<td>8,700<\/td>\n<td>8\/9<\/td>\n<td>Frequent short troubleshooting lessons<\/td>\n<\/tr>\n<tr>\n<td>Meridian Wi-Fi Workshop<\/td>\n<td>4<\/td>\n<td>0.31<\/td>\n<td>28,000<\/td>\n<td>4\/4<\/td>\n<td>Fewer, longer comparison tutorials<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Coverage:<\/strong> all three channel IDs resolved, all uploads-playlist pages completed, 19 in-window video IDs found, 19 video resources returned, and 18 videos exposed a numeric view count. <strong>Observation:<\/strong> Cedar published most often; Meridian&#8217;s four-video median was highest; Northstar sat between them. <strong>Editorial action:<\/strong> test one original troubleshooting lesson weekly for four weeks, then evaluate it against the team&#8217;s own goal rather than copying another channel&#8217;s titles or cadence.<\/p>\n<p>Do not combine the columns into a \u201ccompetitor score.\u201d The official <a href='https:\/\/developers.google.com\/youtube\/terms\/developer-policies-guide' data-source='primary'>developer policies guide<\/a> permits simple mathematical presentation of API metrics but warns against inaccurate replacement metrics, custom channel scores, rivalry-oriented ranking, audience-demographic estimates, and financial projections. Keep raw public counts, transparent arithmetic, and editorial interpretation visibly separate.<\/p>\n<h2>State what public data cannot answer<\/h2>\n<p>Public view and comment counts cannot reveal conversion rate, watch time, retention, revenue, margin, sponsorship terms, audience demographics, customer quality, or why a video received its views. Subscriber count does not fix those gaps. A title pattern and a high view count may coexist without one causing the other.<\/p>\n<p>Shorts view counting differs from long-form video measurement: the current video resource reference notes that from 31 March 2025 a Shorts <code>viewCount<\/code> counts starts and replays without a minimum watch-time requirement. If the cohort mixes formats, report the mix and avoid treating every view as directly comparable.<\/p>\n<p>A regional search screenshot can add a separate discoverability observation, but it is not an input to the metrics above. If that question matters, use the <a href='\/blog\/collect-youtube-search-results-nodejs-proxies\/'>YouTube search Data API and regional QA guide<\/a> and keep its query, market, language, and capture time in another table.<\/p>\n<h2>Refresh the report without changing its history<\/h2>\n<p>Store each run as a dated snapshot. On the next refresh, reuse the same channel IDs, window definition, request path, field mapping, and metric functions. Do not overwrite July&#8217;s captured counts with August&#8217;s values. If a video disappears, preserve the prior record and note that the current lookup did not return it.<\/p>\n<p>API calls do not need a browser proxy for this workflow. If a separate, permitted regional visibility check needs a stable country route, first follow the <a href='\/blog\/test-if-your-proxy-is-working\/'>exit verification procedure<\/a>. Mexela&#8217;s <a href='https:\/\/mexela.com\/proxy-pricing\/'>proxy plans<\/a> can then be compared for the required country and session duration; the proxy never exposes private channel analytics.<\/p>\n<h2>Frequently asked questions<\/h2>\n<div class='mexela-faq'>\n<h3>Why use uploads playlists instead of channel search?<\/h3>\n<p>The channel resource provides its uploads playlist, and <code>playlistItems.list<\/code> supplies documented pagination over that playlist. It avoids using search ranking as an upload inventory.<\/p>\n<h3>Why use median public views?<\/h3>\n<p>A median is less dominated by one unusually large value than an average. It remains a snapshot measure, so include sample size, count coverage, publication window, and capture time.<\/p>\n<h3>Should a missing view count become zero?<\/h3>\n<p>No. Zero is a reported value; missing is unavailable. Exclude missing counts from the median, show how many numeric counts were present, and investigate retrieval coverage separately.<\/p>\n<h3>Can this report estimate a competitor&#8217;s revenue or audience demographics?<\/h3>\n<p>No. Those are not available from the public fields used here, and inferring them would overstate the evidence.<\/p>\n<\/div>\n<p><!-- mexela-platform-guide:end --><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Build a fixed-window YouTube channel comparison from public API records, transparent calculations, missing-count coverage, and a completed report.<\/p>\n","protected":false},"author":1,"featured_media":689,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[47],"tags":[527,530,534,532,481,485],"_links":{"self":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/688"}],"collection":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/comments?post=688"}],"version-history":[{"count":2,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/688\/revisions"}],"predecessor-version":[{"id":865,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/688\/revisions\/865"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/media\/689"}],"wp:attachment":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/media?parent=688"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/categories?post=688"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/tags?post=688"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}