-
-
Save artzub/02b1b91e5c16a76bdf6c4710dbc105f0 to your computer and use it in GitHub Desktop.
function findPlaylist(compare) { | |
let pageToken; | |
while(true) { | |
const list = YouTube.Playlists.list(['snippet'], { | |
"maxResults": 50, | |
"mine": true, | |
...(pageToken && { pageToken }) | |
}); | |
const item = list.items.find((row) => compare && compare(row)); | |
if (item) { | |
return item; | |
} | |
if (!list.nextPageToken) { | |
return null; | |
} | |
pageToken = list.nextPageToken; | |
} | |
} | |
function addPlaylist() { | |
const title = `wl__${(new Date().getMonth()) + 1}`; | |
const item = findPlaylist(({ snippet }) => snippet.title === title); | |
if (item) { | |
return item; | |
} | |
return YouTube.Playlists.insert( | |
{ | |
'snippet': { | |
title, | |
}, | |
'status': { | |
'privacyStatus': 'private' | |
} | |
}, | |
'snippet,status' | |
); | |
} | |
function insertVideo(playlistId, videoId) { | |
return YouTube.PlaylistItems.insert( | |
{ | |
"snippet": { | |
playlistId, | |
"resourceId": { | |
"kind": "youtube#video", | |
videoId | |
} | |
} | |
}, | |
'snippet' | |
); | |
} | |
function run() { | |
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); | |
const sheet = spreadsheet.getSheets()[0]; | |
const lastRow = sheet.getLastRow(); | |
console.log({ lastRow }); | |
if (lastRow < 1) { | |
return; | |
} | |
// 199 because 1 request for insterting playlist | |
const lastIndex = lastRow > 199 ? 199 : lastRow; | |
console.log(`get range: A1:A${lastIndex}`); | |
spreadsheet.toast(`get range: A1:A${lastIndex}`, "fill playlist"); | |
let range = sheet.getRange(`A1:A${lastIndex}`); | |
const rows = range.getValues().flat().filter(Boolean); | |
console.log({ willProcessing: rows.length }); | |
if (rows.length) { | |
let lastAdded = 0; | |
let error; | |
let lastVideo; | |
try { | |
const pl = addPlaylist(); | |
rows.forEach((videoId, index, arr) => { | |
if (!videoId) { | |
return; | |
} | |
try { | |
insertVideo(pl.id, videoId, arr.length - index); | |
lastVideo = videoId; | |
} | |
catch(err) { | |
if (!err.message.includes('Video not found')) { | |
throw err; | |
} | |
console.log('video not found', videoId); | |
lastVideo = videoId; | |
} | |
lastAdded = index + 1; | |
}); | |
} catch(err) { | |
error = err; | |
} | |
console.log({ lastAdded, lastVideo }); | |
if (lastAdded > 0) { | |
if (!lastVideo) { | |
lastVideo = sheet.getRange('B1:B1').getValue(); | |
} | |
const row = sheet.getLastRow() || 1; | |
if (lastAdded >= row) { | |
lastAdded = row - 1; | |
} | |
console.log({ row, lastAdded }); | |
if (lastAdded > 0) { | |
sheet.deleteRows(1, lastAdded); | |
} else { | |
sheet.getRange('A1:A1').setValue(''); | |
} | |
sheet.getRange('B1:B1').setValue(lastVideo); | |
} | |
if (error) { | |
throw error; | |
} | |
} | |
} | |
function init() { | |
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); | |
spreadsheet.removeMenu('YouTube'); | |
spreadsheet.addMenu('YouTube', [ | |
{ | |
name: 'fill playlist', | |
functionName: 'run', | |
} | |
]); | |
} | |
function onOpen() { | |
init(); | |
} |
const targetRootId = 'contents'; | |
const lastVideoSelector = 'ytd-rich-grid-row:last-of-type ytd-rich-item-renderer:last-child'; | |
const allVideosSelector = `.ytd-rich-grid-renderer#${targetRootId} ytd-rich-grid-media`; | |
const rootNodeSelector = `ytd-rich-grid-renderer > #${targetRootId}`; | |
const delay = (ms = 100) => new Promise((resolve) => { | |
setTimeout(() => { | |
resolve(true); | |
}, ms) | |
}); | |
(async function() { | |
let neededVideoId = prompt('Enter id of the last video', ''); | |
if (!neededVideoId) { | |
alert('The last vidoe id can not be empty'); | |
return; | |
} | |
const checkWatched = async () => { | |
await delay(3e3); | |
console.log('find video by id:', neededVideoId); | |
const found = document.querySelector(`a[href^="/watch?v=${neededVideoId}"]`); | |
if (found) { | |
console.log('found, stop observer'); | |
observer.disconnect(); | |
stageSecond(); | |
return; | |
} | |
const lastVideo = document.querySelector(lastVideoSelector); | |
console.log('last video:', lastVideo); | |
if (lastVideo) { | |
lastVideo.scrollIntoView(); | |
} | |
}; | |
let timer; | |
const runWaiter = () => { | |
if (timer) { | |
clearTimeout(timer); | |
} | |
timer = setTimeout(checkWatched, 2e3); | |
}; | |
const stageSecond = async () => { | |
let videos = Array.from(document.querySelectorAll(allVideosSelector)); | |
if (neededVideoId) { | |
const index = videos.findIndex(item => item.data.videoId === neededVideoId); | |
videos.splice(index, videos.length); | |
} | |
videos = videos | |
.filter(item => !item.data.isWatched) | |
.reverse() | |
.map(video => `<tr><td>${video.data.videoId}</td></tr>`) | |
; | |
document.body.innerHTML = ` | |
<pre style="background: white; color: black"> | |
${videos.join('\n')} | |
</pre> | |
`; | |
}; | |
const itemsContainer = document.querySelector(rootNodeSelector); | |
const mutationListener = (mutationList, observer) => { | |
const list = mutationList.filter(item => item.target.id === targetRootId); | |
if (list.length > 0) { | |
runWaiter(); | |
} | |
}; | |
const observer = new MutationObserver(mutationListener); | |
observer.observe(itemsContainer, { childList: true, subtree: true }); | |
await checkWatched(); | |
})(); |
Thanks man, for the work. I get an error in the console when I run the snippet, below is the message
Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
at addAll (VM865 get list of videos:73:14)
at VM865 get list of videos:76:3
addAll @ VM865 get list of videos:73
(anonymous) @ VM865 get list of videos:76
VM27:2522
Oh, sorry, I forgot that I have this gist :D I've updated my private but that is missed.
So, I fixed!
Oh, sorry, I forgot that I have this gist :D I've updated my private but that is missed. So, I fixed!
Thank you so much, I didn't expect a comment this fast. It's working like a charm now, I can't appreciate your work enough.
All the best!
Oh, sorry, I forgot that I have this gist :D I've updated my private but that is missed. So, I fixed!
Thank you so much, I didn't expect a comment this fast. It's working like a charm now, I can't appreciate your work enough.
All the best!
Thanks! Enjoy!!!
@artzub Now I get this error GoogleJsonResponseException: API call to youtube.playlistItems.insert failed with error: Precondition check failed.
I also created a new spreadsheet and new script but when I click the fill playlist option I get this error.
@artzub Now I get this error GoogleJsonResponseException: API call to youtube.playlistItems.insert failed with error: Precondition check failed.
I also created a new spreadsheet and new script but when I click the fill playlist option I get this error.
Could you check if that is the message about reaching the limit of API calls?
Sometimes that happens.
@artzub Now I get this error GoogleJsonResponseException: API call to youtube.playlistItems.insert failed with error: Precondition check failed.
I also created a new spreadsheet and new script but when I click the fill playlist option I get this error.Could you check if that is the message about reaching the limit of API calls? Sometimes that happens.
I didn't notice anything about reaching the limit of API calls neither in the error message nor email that I got from Google, and I extracted around 5000 videos to my playlist from the subscription feed. I also run the script from another gmail and give it the remaining videos that I want to add them to the playlist but there I get the same error message.
I don't know what is the reason for that error, you might add console.log to the script and try to debug by yourself.
what specifically doesn't work?