Why "must be subscribed to enter" is a checkbox I had to delete from a YouTube giveaway picker

dev.to

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];
}
Enter fullscreen mode Exit fullscreen mode

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;
      }
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Source: dev.to

arrow_back Back to News