Skip to content

Instantly share code, notes, and snippets.

@artzub
Last active March 17, 2024 16:28
Show Gist options
  • Save artzub/02b1b91e5c16a76bdf6c4710dbc105f0 to your computer and use it in GitHub Desktop.
Save artzub/02b1b91e5c16a76bdf6c4710dbc105f0 to your computer and use it in GitHub Desktop.
YT Subs to WL
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();
})();
@artzub
Copy link
Author

artzub commented Oct 20, 2022

not work

what specifically doesn't work?

@I-Jalal
Copy link

I-Jalal commented Feb 5, 2024

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

@artzub
Copy link
Author

artzub commented Feb 5, 2024

Oh, sorry, I forgot that I have this gist :D I've updated my private but that is missed.
So, I fixed!

@I-Jalal
Copy link

I-Jalal commented Feb 5, 2024

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!

@artzub
Copy link
Author

artzub commented Feb 6, 2024

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!!!

@I-Jalal
Copy link

I-Jalal commented Mar 9, 2024

@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
Copy link
Author

artzub commented Mar 17, 2024

@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-Jalal
Copy link

I-Jalal commented Mar 17, 2024

@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.

@artzub
Copy link
Author

artzub commented Mar 17, 2024

To see that happened in the script need to visit to execution and open an execution with an error to see a reason.
image

@I-Jalal
Copy link

I-Jalal commented Mar 17, 2024

image

@artzub
Copy link
Author

artzub commented Mar 17, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment