Skip to content

Instantly share code, notes, and snippets.

@jasontucker
Created November 13, 2025 21:47
Show Gist options
  • Select an option

  • Save jasontucker/9a6289d72d0a409975ead5fb2acc05ec to your computer and use it in GitHub Desktop.

Select an option

Save jasontucker/9a6289d72d0a409975ead5fb2acc05ec to your computer and use it in GitHub Desktop.
// --- GLOBAL CONFIGURATION ---
var CACHE_DURATION_DAYS = 7;
var VIDEO_DURATION_THRESHOLD = 120; // 2 minutes
// NOTE: UPDATE THESE WITH YOUR ACTUAL IDs
var PLAYLIST_ID = 'ADD YOUR ID HERE';
var SPREADSHEET_ID = 'THE SPREADSHEET ID';
var CHANNEL_IDS = [
'UCEJM4kRM01cY-2NeJ4-3nbw', 'UCZkGmgbBziqGCYI3x9hVQPg', 'UC5OdB-sGz9atv-tJINwWOTg',
'UCtf6aq_atnqF6shEam6m1sQ', 'UCsphzld6v6ghCl_RyvihNgA', 'UC4ijq8Cg-8zQKx8OH12dUSw',
'UCQdxE3tUpHOKQ5zK-_fOBjg'
];
// =================================================================
// === 1. MAIN PLAYLIST UPDATE FUNCTION (for Scheduled Run 1) ===
// =================================================================
function updatePlaylist() {
// Ensure the sheet structure is ready
initializeSheet(SPREADSHEET_ID);
var eightDaysAgo = new Date();
eightDaysAgo.setDate(eightDaysAgo.getDate() - 8);
try {
var allVideos = [];
for (var i = 0; i < CHANNEL_IDS.length; i++) {
var videos = getLatestVideos(CHANNEL_IDS[i], eightDaysAgo);
allVideos = allVideos.concat(videos);
}
var allVideoIds = allVideos.map(video => video.id.videoId);
var videoDetails = getVideoDetailsInBatches(allVideoIds);
var watchedVideos = getWatchedVideos(SPREADSHEET_ID);
for (var i = 0; i < allVideos.length; i++) {
var video = allVideos[i];
var videoId = video.id.videoId;
var details = videoDetails[videoId];
// Merge all video data
var fullVideoData = {
id: videoId,
channelName: video.snippet.channelTitle,
title: video.snippet.title,
publishedAt: video.snippet.publishedAt,
durationInSeconds: details ? details.durationInSeconds : 0
};
if (fullVideoData.durationInSeconds > VIDEO_DURATION_THRESHOLD &&
!watchedVideos.includes(videoId) &&
!videoInPlaylist(videoId, PLAYLIST_ID)) {
addToPlaylist(videoId, PLAYLIST_ID);
addToWatchedList(fullVideoData, SPREADSHEET_ID);
Logger.log('Added video ' + videoId + ' to playlist and watched list');
}
}
} catch (e) {
console.error('An error occurred while updating the playlist:', e);
}
}
// =================================================================
// === 2. CLEANUP FUNCTIONS (for Scheduled Run 2) ===
// =================================================================
// Wrapper function for trigger scheduling
function runPlaylistCleanup() {
cleanPlaylist(PLAYLIST_ID, SPREADSHEET_ID);
}
function cleanPlaylist(playlistId, spreadsheetId) {
Logger.log('Starting scheduled playlist cleanup...');
// 1. Get the list of videos marked as watched in the Sheet
var watchedVideoIds = getWatchedVideos(spreadsheetId);
if (watchedVideoIds.length === 0) {
Logger.log('No videos found in the Watched Videos sheet. Cleanup complete.');
return;
}
// 2. Get all item IDs currently in the YouTube playlist
var playlistItems = getAllPlaylistItems(playlistId);
// 3. Iterate and delete
playlistItems.forEach(function(item) {
var videoId = item.snippet.resourceId.videoId;
// Check if the video is watched
if (watchedVideoIds.includes(videoId)) {
try {
// Delete the item using its unique playlist item ID
// Note: The delete operation costs 50 quota units.
YouTube.PlaylistItems.remove(item.id);
Logger.log('Removed playlist item ' + item.id + ' (Video: ' + videoId + ')');
} catch (e) {
// Log error but continue with other deletions
console.error('Error removing item ' + item.id + ': ' + e);
}
}
});
Logger.log('Playlist cleanup finished.');
}
// Helper function to handle paginated results for the playlist items (Cost: 1 unit per 50 items)
function getAllPlaylistItems(playlistId) {
var allItems = [];
var pageToken = '';
do {
// Request 'contentDetails' to get the videoId and 'snippet' to confirm.
var response = YouTube.PlaylistItems.list('snippet,contentDetails', {
playlistId: playlistId,
maxResults: 50,
pageToken: pageToken
});
allItems = allItems.concat(response.items);
pageToken = response.nextPageToken;
} while (pageToken);
return allItems;
}
// =================================================================
// === 3. HELPER FUNCTIONS ===
// =================================================================
function exponentialBackoff(func, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
return func();
} catch (e) {
if (i === maxRetries - 1) throw e;
const waitTime = Math.pow(2, i) * 1000 + Math.random() * 1000;
Utilities.sleep(waitTime);
}
}
}
function initializeSheet(spreadsheetId) {
var ss = SpreadsheetApp.openById(spreadsheetId);
var sheet = ss.getSheetByName('Watched Videos');
if (!sheet) {
sheet = ss.insertSheet('Watched Videos');
}
// Set headers if the first cell is empty or doesn't match
var headerRange = sheet.getRange('A1:D1');
if (headerRange.getValues()[0][0] !== 'Video ID') {
headerRange.setValues([['Video ID', 'Channel Name', 'Video Title', 'Publication Date/Time']]).setFontWeight('bold');
}
}
function getLatestVideos(channelId, publishedAfter, pageToken) {
return exponentialBackoff(() => {
var options = {
channelId: channelId,
type: 'video',
order: 'date',
publishedAfter: publishedAfter.toISOString(),
maxResults: 10
};
if (pageToken) options.pageToken = pageToken;
try {
// 'snippet' is essential here for title, channel name, and date
var results = YouTube.Search.list('id,snippet', options);
return results.items;
} catch (e) {
console.error('Error fetching latest videos:', e);
throw e;
}
});
}
function getVideoDetailsInBatches(videoIds) {
var cache = CacheService.getScriptCache();
var videoDetails = {};
var uncachedIds = [];
videoIds.forEach(function(videoId) {
var cachedData = cache.get(videoId);
if (cachedData) {
videoDetails[videoId] = JSON.parse(cachedData);
} else {
uncachedIds.push(videoId);
}
});
for (var i = 0; i < uncachedIds.length; i += 50) {
var batch = uncachedIds.slice(i, i + 50);
exponentialBackoff(() => {
try {
var response = YouTube.Videos.list('contentDetails', {id: batch.join(',')});
response.items.forEach(function(item) {
var durationInSeconds = parseDuration(item.contentDetails.duration);
videoDetails[item.id] = {durationInSeconds: durationInSeconds};
cache.put(item.id, JSON.stringify({durationInSeconds: durationInSeconds}), 21600);
});
} catch (e) {
console.error('Error fetching video details:', e);
throw e;
}
});
}
return videoDetails;
}
function parseDuration(duration) {
var match = duration.match(/PT(\d+H)?(\d+M)?(\d+S)?/);
if (!match) return 0;
var hours = parseInt(match[1]) ? parseInt(match[1].replace('H','')) : 0;
var minutes = parseInt(match[2]) ? parseInt(match[2].replace('M','')) : 0;
var seconds = parseInt(match[3]) ? parseInt(match[3].replace('S','')) : 0;
return hours * 3600 + minutes * 60 + seconds;
}
function getWatchedVideos(spreadsheetId) {
var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('Watched Videos');
if (!sheet) return [];
// Get all IDs from Column A starting from row 2
return sheet.getRange('A2:A').getValues().flat().filter(String);
}
function videoInPlaylist(videoId, playlistId) {
return exponentialBackoff(() => {
var cache = CacheService.getScriptCache();
var cacheKey = 'playlist_' + videoId;
var cachedResult = cache.get(cacheKey);
if (cachedResult !== null) {
return cachedResult === 'true';
}
try {
var playlistItems = YouTube.PlaylistItems.list('snippet', {
playlistId: playlistId,
videoId: videoId,
maxResults: 1
});
var result = playlistItems.items && playlistItems.items.length > 0;
cache.put(cacheKey, result.toString(), 21600);
return result;
} catch (e) {
console.error('Error checking video in playlist:', e);
throw e;
}
});
}
function addToPlaylist(videoId, playlistId) {
return exponentialBackoff(() => {
try {
YouTube.PlaylistItems.insert({
snippet: {
playlistId: playlistId,
resourceId: {
kind: 'youtube#video',
videoId: videoId
}
}
}, 'snippet');
} catch (e) {
console.error('Error adding video to playlist:', e);
throw e;
}
});
}
function addToWatchedList(videoData, spreadsheetId) {
var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('Watched Videos');
// A: Video ID, B: Channel Name, C: Video Title, D: Publication Date/Time
var row = [
videoData.id,
videoData.channelName,
videoData.title,
videoData.publishedAt
];
sheet.appendRow(row);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment