A YouTuber I know asked me to help pick a winner from a giveaway video fairly, because doing it by scrolling through comments and eyeballing one invites exactly the kind of "the mod's friend won" accusations you'd expect. I figured: grab the comments, roll a die, done — how hard can that be? Turns out, once you actually build it against the real YouTube Data API v3 instead of imagining how it probably works, there's pagination to chase, replies to exclude, duplicate commenters to catch, and one feature I had to rip out because it's technically impossible to build honestly.
Turning whatever URL someone pastes into a video ID
People paste video links in three different shapes — the desktop watch?v= link, the shortened youtu.be/ link, and the shorts/ link — and none of them go through a real URL parser, just string splitting on whichever marker is present:
let searchID = "";
if (searchKey.value.indexOf("v=") > -1) {
searchID = searchKey.value.split("v=")[1];
} else if (searchKey.value.indexOf("youtu.be") > -1) {
// https://youtu.be/KtkW3Hx0MvA
searchID = searchKey.value.split("youtu.be/")[1];
} else if (searchKey.value.indexOf("shorts") > -1) {
// https://www.youtube.com/shorts/QWUTKpZmSX0
searchID = searchKey.value.split("shorts/")[1];
}
This works for a clean link, but it doesn't strip anything that comes after the ID. Paste a link with a timestamp (&t=45s) or a share-tracking suffix (?si=xyz) and that junk rides along as part of searchID, gets sent straight to the video-lookup call, and comes back as a generic "couldn't find that video" error — which is technically true but tells the organizer nothing about why. I didn't catch this until someone pasted a link straight from the YouTube mobile app's share sheet, which appends a tracking param by default.
Paginating through the comments API without losing your place
YouTube's commentThreads endpoint hands back comments in pages of up to 100, with a nextPageToken when there's more. The fetch function just calls itself with that token until there isn't one:
const getComment = (searchID, pageToken = null) => {
isLoading.value = true;
$API.youtubeData.youtube_getComment(searchID, pageToken).then((res) => {
let { data } = res;
if (data && data.items) {
data.items.forEach((item) => {
comentData.push({
authorChannelId: item.snippet.topLevelComment.snippet.authorChannelId,
authorDisplayName: item.snippet.topLevelComment.snippet.authorDisplayName,
likeCount: item.snippet.topLevelComment.snippet.likeCount,
textOriginal: item.snippet.topLevelComment.snippet.textOriginal,
totalReplyCount: item.snippet.totalReplyCount,
// ...a few more fields for the table
});
});
if (data.nextPageToken) {
getComment(searchID, data.nextPageToken);
} else {
isLoading.value = false;
}
}
});
};
Notice everything is pulled from topLevelComment.snippet — never the replies themselves. totalReplyCount is captured just so it can be displayed in the table, but the actual reply text is never fetched. So if a giveaway's rule is "reply to the pinned comment to enter" rather than "leave a top-level comment," this tool structurally can't see those entries — it only ever eats top-level comments, which is also why the UI explicitly labels its count as "comments (excluding replies)."
One entry per commenter, not per comment
Before picking a winner, every comment runs through a filter chain, and the interesting one is duplicate removal:
const MessageFrom = new Set();
let filterList = comentData.filter(({ textDisplay, likeCount, authorChannelId, publishedAt }) => {
if (mustContain.value && textDisplay.indexOf(mustContainValue.value) == -1) return false;
if (mustLength.value && textDisplay.length < mustLengthValue.value) return false;
if (mustLike.value && likeCount * 1 < mustLikeValue.value) return false;
if (mustNoRepeat.value) {
if (MessageFrom.has(authorChannelId)) {
return false;
} else {
MessageFrom.add(authorChannelId);
}
}
// ...date-range check
return true;
});
Dedup is keyed on authorChannelId, not the display name — a name is just cosmetic text a user can change at any moment, so two comments from the same account could show two different names. The channel ID is the one thing that's actually stable. Since Array.filter walks the list in order, whichever comment from a given channel appears first is the one that survives; any later comment from that same account is silently dropped, keyword-spam or not.
The date-range check on the next line is the one piece of this function I'd flag if I were reviewing someone else's PR:
if (
(mustDate.value &&
new Date(new Date(mustDateStart.value).toDateString()).getTime() >
new Date(publishedAt).getTime()) ||
new Date(new Date(mustDateEnd.value).toDateString()).getTime() + 86400000 <
new Date(publishedAt).getTime()
)
return false;
The parentheses only wrap mustDate.value && startCheck — the end-date half of the || has no such guard, so it's evaluated regardless of whether the "date range" checkbox is even ticked. In practice it's harmless today only because an empty mustDateEnd produces Invalid Date, and NaN < anything is always false. But it's one stray value away from silently filtering out legitimate entries the moment someone fills in an end date without checking the box first.
Picking winners without replacement, the brute-force way
Once the pool is filtered, picking N winners isn't a shuffle — it's rejection sampling:
let real_index = [];
for (let i = 0; i < AwardNumber.value; i++) {
let now = random(filterList.length);
if (!real_index.includes(now)) {
real_index.push(now);
} else {
i--; // collision — roll again
}
}
function random(real_max) {
return Math.floor(Math.random() * (real_max - 0)) + 0;
}
Roll a random index; if it's already been picked, back the loop counter up and roll again. It's not a proper Fisher–Yates partial shuffle, and it doesn't need to be — the code already checked earlier that filterList.length >= AwardNumber.value, so the loop is guaranteed to eventually land on N distinct indexes, and for a typical giveaway (a handful of winners out of hundreds of eligible comments) collisions are rare enough that a plain retry loop is simpler to read than a real shuffle and costs nothing anyone would notice.
Where this quietly falls short
There's a checkbox still sitting in the template, commented out, for "must be subscribed to the channel" — with a note next to it admitting that anyone who hasn't made their subscriptions public gets treated as unsubscribed. I left it disabled on purpose: YouTube's API has no endpoint that tells you "does channel A have viewer B subscribed," because a user's subscription list is private by default and there's no way to check it from public comment data without that specific user handing over their own OAuth token. Any tool claiming to enforce a subscribe-to-enter rule automatically is either lying or quietly excluding most of your actual subscribers.
A couple other honest gotchas: the comments API has a daily quota, so a video with an unusually large comment count can exhaust it mid-fetch and just stop with a generic error. And dedup by channel ID stops someone from commenting five times on one account, but does nothing to stop someone running the exact same giveaway comment from two separate Google accounts — there's no way to detect that from a comment thread alone.
I cleaned this up into a small free tool if you run YouTube giveaways and want to skip building your own version: YouTube Comment Picker Tool. No sign-up, just paste a video link.
Available in other languages
- Youtube留言抽獎工具 — 繁體中文
- YouTube评论抽奖工具 — 简体中文
- YouTube Comment Picker Tool — English
- YouTubeコメント抽選ツール — 日本語
- Youtube 댓글 추첨 도구 — 한국어
- Outil de tirage au sort de commentaires Youtube — Français
- Инструмент розыгрыша комментариев на Youtube — Русский
- YouTube-Kommentar-Picker-Tool — Deutsch
- Alat Undian Komentar YouTube — Bahasa Indonesia
- Selector de Comentarios de YouTube — Español
- Công cụ chọn bình luận YouTube — Tiếng Việt
- เครื่องมือจับรางวัลคอมเม้นต์ยูทูบ — ไทย
- Losowanie Komentarzy YouTube — Polski
- YouTube Yorum Çekiliş Aracı — Türkçe
- Estrattore Commenti YouTube — Italiano
- Sorteador de Comentários do YouTube — Português
- YouTube Reactie Verloting Tool — Nederlands
- Інструмент для витягування коментарів на YouTube — Українська