Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ This is done using Google Sheets for interface, Google Script + Youtube API v3 f

- Skip adding videos less than 3 minute in length (e.g. shorts). Note that this does not filter exclusively to shorts and removes all videos less than 3 mins (optional)

- Can filter videos from channels or playlists by title (optional)

### (Extra) Scripts to easily remove multiple items from a youtube playlist [here](./removeVidsFromPlaylist.md)

# Where to get help
Expand Down Expand Up @@ -51,6 +53,7 @@ If you ran into problems, here are some of the possible sources for solutions:
- User ID (last part (after last `/`) in `https://www.youtube.com/user/someusername`)
- Channel ID (last part (after last `/`) in `https://www.youtube.com/channel/UCzMVH2jEyEwXPBvyht8xQNw`)
- Playlist ID (last part (after `?list=` in `https://www.youtube.com/playlist?list=PLd0LhgZxFkVKh_JNXcdHoPYo832Wu9fub`)
- A filter string to filter videos by title, followed by a pipe symbol `|` and then the Channel ID or Playlist ID
- `ALL`, to add all new videos from all of your subscriptions
- NOTE: custom URLs cannnot be used (i.e. the last part of `https://www.youtube.com/c/skate702`). Please get the channel's ID as described in the Troubleshooting section under `Cannot query for user <USERNAME>`
- Optionally add a number of days in column C. The playlist in this row will not be updated until that many days have passed.
Expand Down
100 changes: 96 additions & 4 deletions sheetScript.gs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ function updatePlaylists(sheet) {
} else {
/// ...get channels...
var channelIds = [];
var filterChannelIds = [];
var filterPlaylistIds = [];
var playlistIds = [];
for (var iColumn = reservedTableColumns; iColumn < sheet.getLastColumn(); iColumn++) {
var channel = data[iRow][iColumn];
Expand All @@ -107,6 +109,15 @@ function updatePlaylists(sheet) {
else [].push.apply(channelIds, newChannelIds);
} else if (channel.substring(0,2) == "PL" && channel.length > 10) // Add videos from playlist. MaybeTODO: better validation, since might interpret a channel with a name "PL..." as a playlist ID
playlistIds.push(channel);
else if(channel.substring(0,2) == "F:" && channel.split("|").length == 2) // Check for a filter beginning with "F:" and terminated with a pipe symbol, with the channel ID after the pipe symbol.
{
//Check if ID is a channel vs a playlist
if(channel.split("|")[1].substring(0,2) == "UC" && channel.length > 10) {
filterChannelIds.push({ filter: channel.split("|")[0].substring(2), id: channel.split("|")[1]});
} else if(channel.split("|")[1].substring(0,2) == "PL" && channel.length > 10) {
filterPlaylistIds.push({ filter: channel.split("|")[0].substring(2), id: channel.split("|")[1]});
}
}
else if (!(channel.substring(0,2) == "UC" && channel.length > 10)) // Check if it is not a channel ID (therefore a username). MaybeTODO: do a better validation, since might interpret a channel with a name "UC..." as a channel ID
{
try {
Expand Down Expand Up @@ -136,6 +147,26 @@ function updatePlaylists(sheet) {
[].push.apply(newVideoIds, videoIds);
}
}
/// ...get FILTERED videos from the channels...
for (var i = 0; i < filterChannelIds.length; i++) {
var videoIds = getVideoIdsFiltered(filterChannelIds[i].id, lastTimestamp, filterChannelIds[i].filter)
if (!videoIds || typeof(videoIds) !== "object") addError("Failed to get filtered videos with channel id "+filterChannelIds[i].id)
else if (debugFlag_logWhenNoNewVideosFound && videoIds.length === 0) {
Logger.log("Channel with id "+filterChannelIds[i].id+" has no new videos after filtering")
} else {
[].push.apply(newVideoIds, videoIds);
}
}
/// ...get FILTERED videos from the playlists...
for (var i = 0; i < filterPlaylistIds.length; i++) {
var videoIds = getPlaylistVideoIds(filterPlaylistIds[i].id, lastTimestamp, filterPlaylistIds[i].filter)
if (!videoIds || typeof(videoIds) !== "object") addError("Failed to get filtered videos with playlist id "+filterPlaylistIds[i].id)
else if (debugFlag_logWhenNoNewVideosFound && videoIds.length === 0) {
Logger.log("Playlist with id "+filterPlaylistIds[i].id+" has no new videos after filtering")
} else {
[].push.apply(newVideoIds, videoIds);
}
}
for (var i = 0; i < playlistIds.length; i++) {
var videoIds = getPlaylistVideoIds(playlistIds[i], lastTimestamp)
if (!videoIds || typeof(videoIds) !== "object") addError("Failed to get videos with playlist id "+playlistIds[i])
Expand Down Expand Up @@ -395,8 +426,64 @@ function getVideoIdsWithLessQueries(channelId, lastTimestamp) {
return videoIds.reverse(); // Reverse to get videos in ascending order by date
}

// Get Video IDs from Playlist
function getPlaylistVideoIds(playlistId, lastTimestamp) {
// Get videos from Channels and then get their titles for filtering
// slower and date ordering is a bit messy, and more expensive for quota than
// using the search API but I couldn't make that work reliably
function getVideoIdsFiltered(channelId, lastTimestamp, filterString) {
var videoIds = [];
var uploadsPlaylistId;
try {
// Check Channel validity
var results = YouTube.Channels.list('contentDetails', {
id: channelId
});
if (!results) {
addError("YouTube channel search returned invalid response for channel with id "+channelId)
return []
} else if (!results.items || results.items.length === 0) {
addError("Cannot find channel with id "+channelId)
return []
} else {
uploadsPlaylistId = results.items[0].contentDetails.relatedPlaylists.uploads;
}
} catch (e) {
addError("Cannot search YouTube for channel with id "+channelId+", ERROR: " + "Message: [" + e.message + "] Details: " + JSON.stringify(e.details));
return [];
}

nextPageToken = ''
do {
try {
var results = YouTube.PlaylistItems.list('snippet,contentDetails', {
playlistId: uploadsPlaylistId,
maxResults: 50,
order: "date",
pageToken: nextPageToken
})
var filteredVideosToBeAdded = results.items.filter(function (vid) {return ((new Date(lastTimestamp)) <= (new Date(vid.contentDetails.videoPublishedAt)))})
var videosToBeAdded = filteredVideosToBeAdded.filter(vid => vid.snippet.title.toLowerCase().includes(filterString.toLowerCase()));
if (videosToBeAdded.length == 0) {
break;
} else {
[].push.apply(videoIds, videosToBeAdded.map(function (vid) {return vid.contentDetails.videoId}));
}
nextPageToken = results.nextPageToken;
} catch (e) {
if (e.details.code !== 404) { // Skip error count if Playlist isn't found, then channel is empty
addError("Cannot search YouTube with playlist id "+uploadsPlaylistId+", ERROR: Message: [" + e.message + "] Details: " + JSON.stringify(e.details));
} else {
Logger.log("Warning: Channel "+channelId+" does not have any uploads in "+uploadsPlaylistId+", ignore if this is intentional as this will not fail the script. API error details for troubleshooting: " + JSON.stringify(e.details));
}
return [];
}
} while (nextPageToken != null);

return videoIds.reverse(); // Reverse to get videos in ascending order by date
}


// Get Video IDs from Playlist, with filtering support
function getPlaylistVideoIds(playlistId, lastTimestamp, filterString=null) {
var videoIds = [];
var nextPageToken = '';
while (nextPageToken != null){
Expand All @@ -417,8 +504,13 @@ function getPlaylistVideoIds(playlistId, lastTimestamp) {
break
}

for (var j = 0; j < results.items.length; j++) {
var item = results.items[j];
if (filterString) {
var videosToBeAdded = results.items.filter(vid => vid.snippet.title.toLowerCase().includes(filterString.toLowerCase()));
} else {
var videosToBeAdded = results.items;
}
for (var j = 0; j < videosToBeAdded.length; j++) {
var item = videosToBeAdded[j];
if ((new Date(item.snippet.publishedAt)) > (new Date(lastTimestamp)))
videoIds.push(item.snippet.resourceId.videoId);
}
Expand Down