YouTube Video Scoring, Video Success Analysis and Score Overlay Script

In the past, I used to make these calculations manually to make research on YouTube and analyze which contents, topics or content types are successful relative to each channel and its video views.

It’s a bit of an advanced tool, but if you figure out how to use it, you’ll have a golden tool in your hands. 🙂

I improved the score calculation formula with the AI model. I think it’s a bit better than before, though it’s still not perfect. If a channel is very big like a couple of million subscribers but only gets 30-40k views, it ends up with a score of 0 🤣 It’s a bit brutal, but I think it’s fine for those edge cases.

How the Score Calculated;

  • Relative Ratio & Logarithmic Mapping:
    computing the ratio (views/(subs + 1)) and then take its base‑10 logarithm. The quadratic function in that logarithmic domain was chosen so that the function passes through roughly these points:
    • ratio = 0.1 → score ≈ 10
    • ratio = 0.5 → score ≈ 50
    • ratio = 1 → score ≈ 62
    • ratio = 10 → score ≈ 80
  • Clamping:
    clamp the score between 0 and 100.

Script works on search page since thats where you make your research depending on your topic and search keywords..

YouTube Data API v3 daily free quota finishes really really fast keep it mind use carefully 🙂

When you fill your daily quota it will start to give 403 errors.

For bigger setup using https://dataforseo.com/update/subscribers-count-in-youtube-video-info-api would be better.

Google creating obscrutity and complexity where you need simplicity this will be their doom at the end they just dont know it yet..

(function() {
  // Cache objects to store results for faster repeated access.
  const channelCache = {};
  const handleCache = {};

  // Your YouTube Data API key from Google Cloud Console.
  const API_KEY = 'YOUR_YOUTUBE_V3_API_KEY_HERE';

  // Helper function to create a delay.
  function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Utility to parse formatted numbers (e.g., "1.2M" -> 1200000)
  function parseCount(text) {
    text = text.toLowerCase().replace(/,/g, '').trim();
    let multiplier = 1;
    if (text.includes('k')) multiplier = 1e3;
    else if (text.includes('m')) multiplier = 1e6;
    else if (text.includes('b')) multiplier = 1e9;
    const match = text.match(/([\d\.]+)/);
    const num = match ? parseFloat(match[1]) : 0;
    return num * multiplier;
  }

  // Calculate a score based on views and subscribers.
  // Instead of the old logarithmic approach, we now use a quadratic function
  // in log10(views/(subs+1)) that was calibrated such that:
  // - For a ratio of ~0.1 (e.g., 100 views vs 1,000 subs), the score is around 10.
  // - For a ratio of ~0.5 (e.g., 50,000 views vs 100,000 subs), the score is around 50.
  // - For a ratio of ~10 (e.g., 10,000 views vs 1,000 subs), the score is around 80.
  function calculateScore(views, subs) {
    const ratio = views / (subs + 1);
    if (ratio <= 0) return 0;
    const logRatio = Math.log10(ratio);
    // Quadratic function: score = -17.08 * (log10(ratio))^2 + 35 * log10(ratio) + 62.08.
    // This is calibrated to give roughly: 
    //   ratio = 0.1  -> ~10 score,
    //   ratio = 0.5  -> ~50 score,
    //   ratio = 1    -> ~62 score,
    //   ratio = 10   -> ~80 score.
    const score = -17.08 * Math.pow(logRatio, 2) + 35 * logRatio + 62.08;
    return Math.round(Math.max(0, Math.min(100, score)));
  }

  /**
   * Fetches channel statistics using the YouTube Data API v3.
   * @param {string} channelId - The unique channel ID.
   * @return {Promise<object>} - Resolves with the channel's statistics (including subscriberCount).
   */
  async function fetchChannelStats(channelId) {
    if (channelCache[channelId] !== undefined) {
      return channelCache[channelId];
    }

    // Wait 500ms before making the API call.
    await delay(500);

    const url = `https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${channelId}&key=${API_KEY}`;
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error('Failed to fetch channel data');
      const data = await response.json();
      if (data.items && data.items.length > 0) {
        const stats = data.items[0].statistics;
        channelCache[channelId] = stats;
        return stats;
      } else {
        console.warn(`No data found for channel ID: ${channelId}`);
        return { subscriberCount: "0" };
      }
    } catch (error) {
      console.error('Error fetching channel stats:', error);
      return { subscriberCount: "0" };
    }
  }

  /**
   * Resolves a YouTube handle (without the "@" prefix) into its channel ID using the search endpoint.
   * @param {string} handle - The YouTube handle (without the "@" symbol).
   * @return {Promise<string>} - Resolves with the channel ID if found, or an empty string otherwise.
   */
  async function resolveHandleToChannelId(handle) {
    // Check cache first.
    if (handleCache[handle] !== undefined) return handleCache[handle];

    // Wait 500ms before making the API call.
    await delay(500);

    const query = encodeURIComponent(handle);
    const url = `https://www.googleapis.com/youtube/v3/search?part=id&type=channel&q=${query}&key=${API_KEY}`;
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`Failed to search for handle ${handle}`);
      }
      const data = await response.json();
      if (data.items && data.items.length > 0 && data.items[0].id && data.items[0].id.channelId) {
        const channelId = data.items[0].id.channelId;
        handleCache[handle] = channelId;
        return channelId;
      }
      return '';
    } catch (error) {
      console.error("Error resolving handle", handle, error);
      return '';
    }
  }

  /**
   * Extracts the channel ID from a YouTube channel URL.
   * Supports both full channel URLs (with "/channel/") and handle URLs (starting with "/@").
   * @param {string} url - The channel URL.
   * @return {Promise<string>} - Resolves with the channel ID or an empty string if not found.
   */
  async function extractChannelId(url) {
    // Handle the @username format.
    if (url.startsWith("/@")) {
      // Remove the "/@" from the beginning.
      const handle = url.slice(2);
      return await resolveHandleToChannelId(handle);
    } else {
      // Try to match traditional channel URLs.
      const channelIdMatch = url.match(/(channel\/)?([A-Za-z0-9_-]{24,})/);
      return channelIdMatch ? channelIdMatch[2] : '';
    }
  }

  /**
   * Processes a video element by extracting the view count and the channel URL,
   * then calculating and overlaying a score along with the subscriber count.
   * @param {Element} videoEl - The DOM element representing the video.
   */
  async function processVideo(videoEl) {
    if (videoEl.dataset.scoreAdded) return;
    videoEl.dataset.scoreAdded = "true";

    let viewText = null;
    const spans = videoEl.querySelectorAll('span.inline-metadata-item.style-scope.ytd-video-meta-block');
    for (const span of spans) {
      if (span.textContent.toLowerCase().includes('view')) {
        viewText = span.textContent;
        break;
      }
    }
    if (!viewText) return;
    const views = parseCount(viewText);

    // Adjust the selector as needed based on your page structure.
    const channelLink = videoEl.querySelector('a.yt-simple-endpoint.style-scope.yt-formatted-string');
    if (!channelLink) return;

    const channelUrl = channelLink.getAttribute('href');
    const channelId = await extractChannelId(channelUrl);
    if (!channelId) {
      console.warn(`Cannot extract channel ID from: ${channelUrl}`);
      return;
    }

    const stats = await fetchChannelStats(channelId);
    const subs = parseInt(stats.subscriberCount, 10);

    const score = calculateScore(views, subs);

    // Append an overlay that includes both the score and subscriber count.
    videoEl.style.position = 'relative';
    const scoreDiv = document.createElement('div');
    scoreDiv.style.position = 'absolute';
    scoreDiv.style.top = '0';
    scoreDiv.style.right = '0';
    scoreDiv.style.zIndex = '999';
    scoreDiv.style.background = 'rgba(0, 0, 0, 0.7)';
    scoreDiv.style.color = 'white';
    scoreDiv.style.padding = '2px 4px';
    scoreDiv.style.fontSize = '12px';
    scoreDiv.textContent = 'Score: ' + score + ' | Subs: ' + subs;
    videoEl.appendChild(scoreDiv);
  }

  // Process all video elements on the page.
  function processAllVideos() {
    const videoEls = document.querySelectorAll('#contents ytd-video-renderer');
    videoEls.forEach(videoEl => {
      processVideo(videoEl);
    });
  }

  // Initial processing of existing video elements.
  processAllVideos();

  // Observe DOM changes to process dynamically loaded videos.
  const observer = new MutationObserver(() => {
    processAllVideos();
  });
  const target = document.querySelector('#contents');
  if (target) {
    observer.observe(target, { childList: true, subtree: true });
  }
})();

Hello 🖐 Welcome

I am planning a professional WordPress and Bricks Builder course. If you are Interested register to this newsletter.