Created
November 13, 2025 21:47
-
-
Save jasontucker/9a6289d72d0a409975ead5fb2acc05ec to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // --- 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