/**
* Get the publication date of the first (oldest) video on a specified YouTube channel.
* Author: Mr Shane
* Version 2025-12-15
* Prerequisites: YouTube Data API v3
*/
function getFirstVideoDate() {
const channelHandle = '@Cocomelon';
const channelInfo = getChannelInfoByHandle(channelHandle);
if (!channelInfo) {
Logger.log('Could not find channel information for handle: ' + channelHandle);
return;
}
const { channelId, uploadsPlaylistId } = channelInfo;
if (!uploadsPlaylistId) {
Logger.log('Could not find uploads playlist for the channel ID: ' + channelId);
return;
}
const firstVideoDate = getOldestVideoDateFromPlaylist(uploadsPlaylistId, channelId);
if (firstVideoDate) {
Logger.log(`The first (oldest) video for handle ${channelHandle} was uploaded on: ` + firstVideoDate);
} else {
Logger.log(`No videos found for the channel handle: ${channelHandle}`);
}
}
function getChannelInfoByHandle(channelHandle) {
try {
const response = YouTube.Channels.list('contentDetails', {
forHandle: channelHandle
});
const items = response.items;
if (items && items.length > 0) {
const item = items[0];
const relatedPlaylists = item.contentDetails.relatedPlaylists;
return {
channelId: item.id,
uploadsPlaylistId: relatedPlaylists?.uploads || null
};
}
} catch (e) {
Logger.log(`Error fetching channel info for handle ${channelHandle}: ${e.message}`);
}
return null;
}
function getOldestVideoDateFromPlaylist(playlistId, channelId) {
const videoDates = [];
let pageToken = '';
while (true) {
const playlistResponse = YouTube.PlaylistItems.list('snippet', {
playlistId: playlistId,
maxResults: 50,
pageToken: pageToken
});
if (!playlistResponse.items || playlistResponse.items.length === 0) {
break;
}
for (const item of playlistResponse.items) {
videoDates.push(item.snippet.publishedAt);
}
pageToken = playlistResponse.nextPageToken;
if (!pageToken) {
break;
}
}
if (videoDates.length > 0) {
videoDates.sort((a, b) => new Date(a) - new Date(b));
return videoDates[0];
}
return null;
}
/**
* Fetches video, playlist, and subscription counts for the authenticated user's YouTube account.
* This version breaks down the video AND playlist counts by privacy status.
* Logs the results to the Apps Script execution log.
* Note: You can get similar information from the YouTube panel of your Google Account Dashboard
* @see https://myaccount.google.com/dashboard
* Author: Mr Shane
* Version: 2025-08-11
* Prerequisites: Youtube Data API v3
*/
function getMyYouTubeStats() {
const videoCounts = { public: 0, private: 0, unlisted: 0, total: 0 };
const playlistCounts = { public: 0, private: 0, unlisted: 0, total: 0 };
const stats = {
subscriptions: 'N/A',
comments: 'Not available via public API' // This is the correct, final status.
};
try {
const channelResponse = YouTube.Channels.list('contentDetails', { mine: true });
if (channelResponse && channelResponse.items && channelResponse.items.length > 0) {
const uploadsPlaylistId = channelResponse.items[0].contentDetails.relatedPlaylists.uploads;
let pageToken = null;
do {
const playlistItemsResponse = YouTube.PlaylistItems.list('status', {
playlistId: uploadsPlaylistId,
maxResults: 50,
pageToken: pageToken
});
if (playlistItemsResponse.items) {
playlistItemsResponse.items.forEach(item => {
switch (item.status.privacyStatus) {
case 'public': videoCounts.public++; break;
case 'private': videoCounts.private++; break;
case 'unlisted': videoCounts.unlisted++; break;
}
});
}
pageToken = playlistItemsResponse.nextPageToken;
} while (pageToken);
videoCounts.total = videoCounts.public + videoCounts.private + videoCounts.unlisted;
} else {
Logger.log('Could not find a YouTube channel associated with this account.');
return;
}
let playlistPageToken = null;
do {
const playlistsResponse = YouTube.Playlists.list('status', {
mine: true,
maxResults: 50,
pageToken: playlistPageToken
});
if (playlistsResponse.items) {
playlistsResponse.items.forEach(playlist => {
switch (playlist.status.privacyStatus) {
case 'public': playlistCounts.public++; break;
case 'private': playlistCounts.private++; break;
case 'unlisted': playlistCounts.unlisted++; break;
}
});
}
playlistPageToken = playlistsResponse.nextPageToken;
} while (playlistPageToken);
playlistCounts.total = playlistCounts.public + playlistCounts.private + playlistCounts.unlisted;
stats.subscriptions = getTotalCount_(YouTube.Subscriptions, { mine: true, part: 'id' });
Logger.log('--- Your YouTube Stats (Detailed) ---');
Logger.log('Total Videos: ' + videoCounts.total);
Logger.log(' - Public: ' + videoCounts.public);
Logger.log(' - Private: ' + videoCounts.private);
Logger.log(' - Unlisted: ' + videoCounts.unlisted);
Logger.log('-------------------------------------');
Logger.log('Total Playlists: ' + playlistCounts.total);
Logger.log(' - Public: ' + playlistCounts.public);
Logger.log(' - Private: ' + playlistCounts.private);
Logger.log(' - Unlisted: ' + playlistCounts.unlisted);
Logger.log('-------------------------------------');
Logger.log('Total Active Subscriptions: ' + stats.subscriptions);
Logger.log('Total Comments: ' + stats.comments);
Logger.log('-------------------------------------');
} catch (e) {
Logger.log('Error: ' + e.message);
}
}
/**
* A helper function to paginate through a YouTube API list resource and get a simple total count.
* This is only used for subscriptions.
* @private
*/
function getTotalCount_(resource, options) {
let count = 0;
let pageToken = null;
const requestOptions = { ...options, maxResults: 50 };
do {
requestOptions.pageToken = pageToken;
const response = resource.list(requestOptions.part, requestOptions);
if (response.items) {
count += response.items.length;
}
pageToken = response.nextPageToken;
} while (pageToken);
return count;
}
/**
* Get the video and view counts for a specified channel handle.
* Author: Mr Shane
* Version: 2025-12-16
* Prerequisites: YouTube Data API v3
* IMPORTANT: While UUSH is a common shortcut to find a channel's Shorts, it isn't an "official" public playlist in the same way the standard "Uploads" playlist is. Because of this, the YouTube Data API often returns inconsistent results (missing videos or temporary caching issues) when you iterate through it using PlaylistItems. In other words, if you run the the script multiple times for the same channel then you must expect the results to vary and not be 100% accurate at any time.
*/
function getVideoAndViewsByChannel() {
const channelHandle = '@Cocomelon';
const channelData = getChannelBasicsByHandle(channelHandle);
if (!channelData) return;
const { channelId, totalViews, uploadsId, channelName } = channelData;
const totalVideoCount = getPlaylistStats(uploadsId, false).count;
let shortsStats = { count: 0, views: 0 };
if (totalVideoCount > 0) {
const shortsId = channelId.replace(/^UC/, 'UUSH');
shortsStats = getPlaylistStats(shortsId, true);
}
const longCount = totalVideoCount - shortsStats.count;
const longViews = totalViews - shortsStats.views;
const tableData = [
{ cat: 'Total Videos', vids: totalVideoCount, views: totalViews },
{ cat: 'Shorts', vids: shortsStats.count, views: shortsStats.views },
{ cat: 'Long Videos', vids: longCount, views: longViews },
];
const header = `--- Channel Stats: ${channelHandle} ---\n` +
`Channel Name: ${channelName}\n\n`;
Logger.log(header + formatDynamicTable(tableData));
}
function getChannelBasicsByHandle(handle) {
try {
const res = YouTube.Channels.list('statistics,contentDetails,snippet', { forHandle: handle });
if (!res.items?.length) {
Logger.log(`Channel handle ${handle} not found.`);
return null;
}
const item = res.items[0];
return {
channelId: item.id,
totalViews: parseInt(item.statistics.viewCount, 10) || 0,
uploadsId: item.contentDetails.relatedPlaylists.uploads,
channelName: item.snippet.title,
};
} catch (e) {
Logger.log(`Error fetching channel basics: ${e.message}`);
return null;
}
}
function getPlaylistStats(playlistId, fetchViews) {
if (!fetchViews) {
try {
const res = YouTube.PlaylistItems.list('id', { playlistId, maxResults: 1 });
return { count: res.pageInfo?.totalResults || 0, views: 0 };
} catch (e) { return { count: 0, views: 0 }; }
}
let stats = { count: 0, views: 0 };
let pageToken = '';
try {
do {
const plRes = YouTube.PlaylistItems.list('contentDetails', { playlistId, maxResults: 50, pageToken });
if (!plRes.items?.length) break;
stats.count += plRes.items.length;
const ids = plRes.items.map(i => i.contentDetails.videoId).join(',');
const vidRes = YouTube.Videos.list('statistics', { id: ids });
if (vidRes.items) {
stats.views += vidRes.items.reduce((sum, v) => sum + (parseInt(v.statistics.viewCount, 10) || 0), 0);
}
pageToken = plRes.nextPageToken;
} while (pageToken);
} catch (e) {
const isMissing = e.message.includes('cannot be found') || e.message.includes('playlist identified');
if (isMissing) {
return stats;
}
Logger.log(`Error processing playlist ${playlistId}: ${e.message}`);
}
return stats;
}
function formatDynamicTable(data) {
const fmt = n => Math.round(n).toLocaleString();
const headers = ['Category', 'Videos', 'Views', 'Avg Views/Video'];
const rows = data.map(r => [
r.cat,
fmt(r.vids),
fmt(r.views),
fmt(r.vids > 0 ? r.views / r.vids : 0)
]);
const all = [headers, ...rows];
const widths = headers.map((_, i) => Math.max(...all.map(row => row[i].length)) + 2);
const pad = (txt, i) => txt[i === 0 ? 'padEnd' : 'padStart'](widths[i]);
const buildLine = (arr) => arr.map((c, i) => pad(c, i)).join('|');
return [
buildLine(headers),
widths.map(w => '-'.repeat(w)).join('|'),
...rows.map(buildLine)
].join('\n');
}
/**
* Code.gs
* Get the video and view counts for a specified channel handle.
* Author: Mr Shane
* Version: 2025-12-24
* Prerequisites: YouTube Data API v3
* IMPORTANT: While UUSH is a common shortcut to find a channel's Shorts, it isn't an "official" public playlist in the same way the standard "Uploads" playlist is. Because of this, the YouTube API often returns inconsistent results (missing videos or temporary caching issues) when you iterate through it using PlaylistItems. In other words, if you run the the script multiple times for the same channel then you must expect the results to vary and not be 100% accurate at any time.
*/
function doGet(){
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('YouTube Stats')
.addMetaTag('viewport','width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getChannelStats(input){
if(!input)return{error:'Please enter a handle, URL, or Channel ID.'};
const channelData=resolveChannel(input);
if(!channelData)return{error:'Channel not found. Try the exact Handle (@Name) or Channel ID.'};
const {channelId,channelName,handle,subscriberCount}=channelData;
const shortsId=channelId.replace(/^UC/,'UUSH');
const shortsStats=getPlaylistStats(shortsId);
const longId=channelId.replace(/^UC/,'UULF');
const longStats=getPlaylistStats(longId);
const totalVids=shortsStats.count+longStats.count;
const totalViews=shortsStats.views+longStats.views;
const firstLongDate=longStats.oldestDate?formatDate(longStats.oldestDate):'-';
const firstShortDate=shortsStats.oldestDate?formatDate(shortsStats.oldestDate):'-';
return{
success:true,
channelName:channelName,
handle:handle,
channelId:channelId,
subscriberCount:subscriberCount,
firstVideoDate:firstLongDate,
firstShortDate:firstShortDate,
data:[
{cat:'Total',vids:totalVids,views:totalViews},
{cat:'Longs',vids:longStats.count,views:longStats.views},
{cat:'Shorts',vids:shortsStats.count,views:shortsStats.views}
]
};
}
function resolveChannel(rawInput){
let q=rawInput.toString().trim();
const idMatch=q.match(/(UC[\w-]{22})/);
if(idMatch){
try{
const res=YouTube.Channels.list('statistics,contentDetails,snippet',{id:idMatch[1]});
if(res.items?.length)return parseChannel(res.items[0]);
}catch(e){}
}
try{q=decodeURIComponent(q);}catch(e){}
q=q.replace(/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\//i,'');
q=q.replace(/^(channel\/|c\/|user\/)/i,'');
q=q.split(/[/?#]/)[0];
q=q.replace(/^@/,'');
if(!q)return null;
const tryHandle='@'+q;
try{
const res=YouTube.Channels.list('statistics,contentDetails,snippet',{forHandle:tryHandle});
if(res.items?.length)return parseChannel(res.items[0]);
}catch(e){}
try{
const search=YouTube.Search.list('id',{q:q,type:'channel',maxResults:1});
if(search.items?.length){
const foundId=search.items[0].id.channelId;
const res=YouTube.Channels.list('statistics,contentDetails,snippet',{id:foundId});
if(res.items?.length)return parseChannel(res.items[0]);
}
}catch(e){}
return null;
}
function parseChannel(item){
return{
channelId:item.id,
totalViews:parseInt(item.statistics.viewCount,10)||0,
subscriberCount:parseInt(item.statistics.subscriberCount,10)||0,
uploadsId:item.contentDetails.relatedPlaylists.uploads,
channelName:item.snippet.title,
handle:item.snippet.customUrl||'No Handle'
};
}
function getPlaylistStats(playlistId){
let stats={count:0,views:0,oldestDate:null};
let pageToken='';
try{
do{
const plRes=YouTube.PlaylistItems.list('contentDetails',{playlistId:playlistId,maxResults:50,pageToken:pageToken});
if(!plRes.items?.length)break;
stats.count+=plRes.items.length;
const lastItem=plRes.items[plRes.items.length-1];
if(lastItem.contentDetails?.videoPublishedAt){
stats.oldestDate=lastItem.contentDetails.videoPublishedAt;
}
const ids=plRes.items.map(i=>i.contentDetails.videoId).join(',');
const vidRes=YouTube.Videos.list('statistics',{id:ids});
if(vidRes.items){
stats.views+=vidRes.items.reduce((sum,v)=>sum+(parseInt(v.statistics.viewCount,10)||0),0);
}
pageToken=plRes.nextPageToken;
}while(pageToken);
}catch(e){return stats;}
return stats;
}
function formatDate(isoString){
if(!isoString)return'-';
const d=new Date(isoString);
return d.toISOString().split('T')[0];
}
<!-- Index.html -->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<style>
html,body{min-height:100vh}
body{font-family:'Roboto',-apple-system,sans-serif;background-color:#f4f6f8;margin:0;padding:20px;display:flex;flex-direction:column;align-items:center}
.container{background:white;max-width:500px;width:100%;padding:20px;border-radius:12px;box-shadow:0 4px 10px rgba(0,0,0,0.1);box-sizing:border-box}
h2{margin-top:0;color:#cc0000;text-align:center}
.input-group{display:flex;gap:10px;margin-bottom:20px}
input{flex-grow:1;padding:12px;border:1px solid #ddd;border-radius:8px;font-size:16px;outline:none;-webkit-appearance:none}
input:focus{border-color:#cc0000}
button{background-color:#cc0000;color:white;border:none;padding:12px 20px;border-radius:8px;font-size:16px;font-weight:bold;cursor:pointer}
button:disabled{background-color:#e0e0e0;color:#999}
#resultArea{display:none}
.info-box{background:#f9f9f9;padding:12px;border-radius:8px;margin-bottom:15px;font-size:13px;border:1px solid #eee}
.info-row{display:flex;justify-content:space-between;margin-bottom:6px}
.info-label{font-weight:bold;color:#666}
.info-value{color:#222;text-align:right;word-break:break-all;margin-left:10px}
.divider{height:1px;background:#eee;margin:8px 0}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;color:#555;font-size:12px;text-transform:uppercase;border-bottom:2px solid #eee;padding:8px}
td{padding:6px 8px;border-bottom:1px solid #f0f0f0}
.num{text-align:right;font-family:monospace;font-size:1.0em;color:#333}
th.num{text-align:right}
#loading{display:none;text-align:center;color:#666;margin-top:20px;font-style:italic}
#error{color:#d32f2f;background:#ffebee;padding:10px;border-radius:6px;display:none;margin-bottom:15px;text-align:center}
</style>
</head>
<body>
<div class="container">
<h2>Channel Stats</h2>
<div class="input-group">
<input type="text" id="handleInput" placeholder="URL, Handle, or Channel ID"/>
<button id="btn" onclick="fetchStats()">Go</button>
</div>
<div id="loading">Analyzing channel data...</div>
<div id="error"></div>
<div id="resultArea">
<div class="info-box">
<div class="info-row"><span class="info-label">Name:</span><span class="info-value" id="outName"></span></div>
<div class="info-row"><span class="info-label">Handle:</span><span class="info-value" id="outHandle"></span></div>
<div class="info-row"><span class="info-label">ID:</span><span class="info-value" id="outId"></span></div>
<div class="info-row"><span class="info-label">Subscribers:</span><span class="info-value" id="outSubs"></span></div>
<div class="divider"></div>
<div class="info-row"><span class="info-label">First Long:</span><span class="info-value" id="outFirstLong"></span></div>
<div class="info-row"><span class="info-label">First Short:</span><span class="info-value" id="outFirstShort"></span></div>
</div>
<table id="statsTable">
<thead>
<tr>
<th>Category</th>
<th class="num">Videos</th>
<th class="num">Views</th>
<th class="num">Avg</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
<script>
function fetchStats(){
const input=document.getElementById('handleInput').value.trim();
if(!input)return;
const btn=document.getElementById('btn'),loading=document.getElementById('loading'),resultArea=document.getElementById('resultArea'),errorDiv=document.getElementById('error');
btn.disabled=true;
btn.innerText='...';
loading.style.display='block';
resultArea.style.display='none';
errorDiv.style.display='none';
google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onFailure).getChannelStats(input);
}
function onSuccess(response){
resetUI();
if(response.error){
const errDiv=document.getElementById('error');
errDiv.innerText=response.error;
errDiv.style.display='block';
return;
}
document.getElementById('outName').innerText=response.channelName;
document.getElementById('outHandle').innerText=response.handle;
document.getElementById('outId').innerText=response.channelId;
document.getElementById('outSubs').innerText=response.subscriberCount.toLocaleString();
document.getElementById('outFirstLong').innerText=response.firstVideoDate;
document.getElementById('outFirstShort').innerText=response.firstShortDate;
const tbody=document.getElementById('tableBody');
tbody.innerHTML='';
response.data.forEach(row=>{
const avg=row.vids>0?Math.round(row.views/row.vids):0;
const tr=document.createElement('tr');
tr.innerHTML=`<td>${row.cat}</td><td class="num">${row.vids.toLocaleString()}</td><td class="num">${row.views.toLocaleString()}</td><td class="num">${avg.toLocaleString()}</td>`;
tbody.appendChild(tr);
});
document.getElementById('resultArea').style.display='block';
}
function onFailure(err){
resetUI();
const errDiv=document.getElementById('error');
errDiv.innerText="System Error: "+err.message;
errDiv.style.display='block';
}
function resetUI(){
document.getElementById('btn').disabled=false;
document.getElementById('btn').innerText='Go';
document.getElementById('loading').style.display='none';
}
</script>
</body>
</html>