Was about that old in that movie

IMDb movies - hovering actors avatars would show how old they were when that movie was released

// ==UserScript==
// @name           Was about that old in that movie
// @namespace      https://github.com/FlowerForWar/was-about-that-old-in-that-movie
// @description    IMDb movies - hovering actors avatars would show how old they were when that movie was released
// @version        0.09
// @author         FlowrForWar
// @include        /https:\/\/www\.imdb\.com\/title\/tt\d+\/($|\?.+)/
// @include        /https:\/\/www\.imdb\.com\/name\/nm\d+\/($|\?.+|#.+)/
// @grant          GM.getValue
// @grant          GM.setValue
// @grant          GM.listValues
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_listValues
// @compatible     edge Tampermonkey or Violentmonkey
// @compatible     firefox Greasemonkey, Tampermonkey or Violentmonkey
// @compatible     chrome Tampermonkey or Violentmonkey
// @compatible     opera Tampermonkey or Violentmonkey
// @supportURL     https://github.com/FlowerForWar/was-about-that-old-in-that-movie
// @license        MIT
// ==/UserScript==

// Gifs showing examples
// https://raw.githubusercontent.com/FlowerForWar/was-about-that-old-in-that-movie/main/example.gif
// https://raw.githubusercontent.com/FlowerForWar/was-about-that-old-in-that-movie/main/example-2.gif

let zodiac_signs_disabled;
let movieFirstRelease;
let globalActorBirthDate;
// console.log(await getStorageKeys());
// console.log('zodiac-signs-disabled: ', zodiac_signs_disabled);

(async function () {
  zodiac_signs_disabled = (await getStorageValue('zodiac-signs-disabled')) || !1;

  if (location.href.startsWith('https://www.imdb.com/name/nm')) {
    const timeTag = document.querySelector('#name-born-info > time[datetime]');
    if (!timeTag || !/^\d{4}-\d{1,2}-\d{1,2}$/.test(timeTag.dateTime)) {
      // alert('No birth date available');
      return;
    }

    const [, month, day] = timeTag.dateTime.split('-').map((string) => Number(string));
    if (month === 0 || day === 0) {
      // alert('Incomplete birth date');
      return;
    }

    globalActorBirthDate = timeTag.dateTime;

    const moviesNodes = [
      ...(
        document.getElementById('filmo-head-actor') ||
        document.getElementById('filmo-head-actorMovie') ||
        document.getElementById('filmo-head-actress') ||
        document.getElementById('filmo-head-actressMovie')
      ).nextElementSibling.querySelectorAll('.filmo-row'),
    ].map((element) => {
      return element.querySelector('span.year_column');
    });
    for (let index = 0; index < moviesNodes.length; index++) {
      const movieNode = moviesNodes[index];
      movieNode.style.setProperty('cursor', 'default', 'important');
      movieNode.addEventListener('mouseenter', moviesNodesHandler);
    }

    const age = Math.floor((new Date() - new Date(globalActorBirthDate)) / 31536000000);
    const dead = !!document.getElementById('name-death-info');
    if (zodiac_signs_disabled && dead) {
      return;
    }
    const age_sign_string = [
      //
      '(',
      !dead && `age ${age}`,
      !dead && !zodiac_signs_disabled && ', ',
      !zodiac_signs_disabled && getZodiacSign(month, day),
      ')',
    ]
      .filter(Boolean)
      .join('');
    timeTag.insertAdjacentText('afterend', age_sign_string);
    // timeTag.insertAdjacentText('afterend', '(' + (!dead ? `age ${age}, ` : '') + tropicalZodiac(month, day) + ')');

    return;
  }

  /* if (document.querySelector('.ipc-inline-list.ipc-inline-list--show-dividers[data-testid="hero-title-block__metadata"]').textContent.startsWith('TV')) {
		// alert('TV Series are not supported!');
		return;
	} */
  if (/\((TV|Podcast|Music|Video|Short) /.test(document.title) && !document.title.includes('Movie')) return;
  // Short || TV Short | Video Game | Video | Music Video | TV Series | Podcast Series | TV Mini Series

  const releaseDates = await getMovieReleaseDates(location.origin, location.pathname);
  if (!releaseDates.length) return void alert('No valid release date');

  movieFirstRelease = releaseDates.sort((first, second) => Date.parse(first) - Date.parse(second))[0];
  console.log(`Movie first release: ${movieFirstRelease}`);

  // const avatarSelector = 'section.ipc-page-section.ipc-page-section--base[cel_widget_id="StaticFeature_Cast"] .ipc-lockup-overlay';
  const avatarSelector = 'section.ipc-page-section.ipc-page-section--base[cel_widget_id="StaticFeature_Cast"] div[data-testid="title-cast-item"]';
  // const avatarsNodes = [...document.querySelectorAll(avatarSelector)];
  const avatarsNodes = document.querySelectorAll(avatarSelector);
  for (let index = 0; index < avatarsNodes.length; index++) {
    const [avatarsNode, nameNode] = [...avatarsNodes[index].querySelectorAll('a[href*="/name/"]')];
    avatarsNode.addEventListener('mouseenter', avatarsNodesHandler);
    nameNode.addEventListener('mouseenter', avatarsNodesHandler);
  }
})();

async function moviesNodesHandler() {
  let cursor_under_element = !0;
  this.removeEventListener('mouseenter', moviesNodesHandler);
  const aElement = this.closest('.filmo-row').querySelector('a[href*="/title/"]');
  const movie = aElement.textContent;
  this.addEventListener('mouseleave', function () {
    aElement.textContent = movie;
    cursor_under_element = !1;
  });
  // aElement.textContent += '..';

  const { origin, pathname } = new URL(aElement.href);
  const releaseDates = await getMovieReleaseDates(origin, pathname);

  if (!releaseDates.length) {
    aElement.textContent = movie;
    this.setAttribute('title', 'No valid release date | or not supported');
    return;
  }
  movieFirstRelease = releaseDates.sort((first, second) => Date.parse(first) - Date.parse(second))[0];

  const info = {
    // movie: this.textContent,
    name: document.querySelector('.name-overview-widget__section h1.header span.itemprop').textContent,
    'age-when-the-movie-first-released': Math.floor((new Date(movieFirstRelease) - new Date(globalActorBirthDate)) / 31536000000),
    'movie-is-not-released': new Date() - new Date(movieFirstRelease) < 0,
  };

  const deathElement = document.querySelector('#name-death-info > time[datetime]');
  const yearElementTitle = [
    //
    movieFirstRelease,
    !deathElement && `\n\nNow, ${info.name} is ${Math.floor((new Date() - new Date(globalActorBirthDate)) / 31536000000)} years old`,
    deathElement && `\n\n${info.name}, died at the age of ${Math.floor((new Date(deathElement.dateTime) - new Date(globalActorBirthDate)) / 31536000000)}`,
  ]
    .filter(Boolean)
    .join('');
  this.setAttribute('title', yearElementTitle);

  const hover_string = `${movie} (${info['movie-is-not-released'] ? 'will be' : 'was'} about ${info['age-when-the-movie-first-released']} years old)`;
  if (cursor_under_element) {
    aElement.textContent = hover_string;
  }
  // const autoHide = setTimeout(() => (aElement.textContent = movie), 5000);

  this.addEventListener('mouseenter', function () {
    // clearTimeout(autoHide);
    aElement.textContent = hover_string;
  });
}

async function avatarsNodesHandler() {
  this.removeEventListener('mouseenter', avatarsNodesHandler);

  const parentNode = this.closest('div[data-testid="title-cast-item"]');
  if (parentNode.dataset.processed) {
    return;
  }

  parentNode.dataset.processed = !0;

  const siblingElement = parentNode.lastChild;

  const a = document.createElement('a');
  a.setAttribute('class', 'title-cast-item__eps-toggle');
  a.style.setProperty('cursor', 'default');
  const span = document.createElement('span');
  span.innerHTML = 'Loading data..';
  a.appendChild(span);
  siblingElement.appendChild(a);

  const response = await fetch(siblingElement.firstElementChild.href.split('?')[0].replace(/\/$/, '') + '/bio');
  const responseText = await response.text();
  const actorDates = [];
  responseText.replace(/datetime="(\d{4}-\d{1,2}-\d{1,2})"/g, (match, date) => actorDates.push(date));
  const [actorBirthDate, actorDeathDate] = actorDates;

  // https://www.imdb.com/title/tt1155076/
  const [, month, day] = (actorBirthDate || '').split('-').map((string) => Number(string));
  if (!actorBirthDate || month === 0 || day === 0) {
    span.style.setProperty('text-decoration-line', 'line-through', 'important');
    span.innerHTML = 'No birth date available';
    return;
  }
  const actorInfo = {
    name: siblingElement.firstElementChild.innerText,
    'age-when-the-movie-first-released': Math.floor((new Date(movieFirstRelease) - new Date(actorBirthDate)) / 31536000000),
    'age-now': Math.floor((new Date() - new Date(actorBirthDate)) / 31536000000),
    'age-at-death': !!actorDeathDate && Math.floor((new Date(actorDeathDate) - new Date(actorBirthDate)) / 31536000000),
    'movie-is-not-released': new Date() - new Date(movieFirstRelease) < 0,
  };

  if (actorDeathDate) span.style.setProperty('color', 'crimson', 'important');
  const age_sign_string = [
    //
    actorInfo['movie-is-not-released'] ? 'Will be' : 'Was',
    ' about ',
    actorInfo['age-when-the-movie-first-released'],
    ' years old',
    !zodiac_signs_disabled && `<br>(${getZodiacSign(month, day)})`,
  ]
    .filter(Boolean)
    .join('');
  span.innerHTML = age_sign_string;

  if (actorDeathDate) {
    a.setAttribute('title', `${actorInfo.name}, died at the age of ${actorInfo['age-at-death']}`);
    this.setAttribute('title', `${actorInfo.name}, died at the age of ${actorInfo['age-at-death']}`);
    parentNode.querySelector('a[href*="/name/"]:not([title])').setAttribute('title', `${actorInfo.name}, died at the age of ${actorInfo['age-at-death']}`);
  } else {
    a.setAttribute('title', `Now, ${actorInfo.name} is ${actorInfo['age-now']} years old`);
    this.setAttribute('title', `Now, ${actorInfo.name} is ${actorInfo['age-now']} years old`);
    parentNode.querySelector('a[href*="/name/"]:not([title])').setAttribute('title', `Now, ${actorInfo.name} is ${actorInfo['age-now']} years old`);
  }
}

async function getMovieReleaseDates(origin, pathname) {
  // releaseinfo page document title may not match that of the original
  // example https://www.imdb.com/title/tt2205529/releaseinfo and https://www.imdb.com/title/tt2205529/
  // So will get the document's title frome the original page first
  // const originalResponse = await fetch(origin + pathname);
  // const originalResponseText = await originalResponse.text();

  const response = await fetch(origin + pathname + 'releaseinfo');
  const responseText = await response.text();

  // view-source:https://www.imdb.com/title/tt2205529/ 's title needs decoding
  const documentTitle = htmlDecode(responseText.match(/<title>(.+?)<\/title>/)[1]);
  // alert(documentTitle);
  // const TV_Series = /<title>.+?\((TV|Podcast|Music|Video|Short) .+?<\/title>/.test(responseText);
  // alert(TV_Series);
  // if (TV_Series) return [];
  if (/\((TV|Podcast|Music|Video|Short) /.test(documentTitle) && !documentTitle.includes('Movie')) return [];

  const regex = /class="release-date-item__date" align="right">(\d{1,2} [a-zA-Z0-9_]+ \d{4})<\/td>/g;
  const releaseDates = [];
  responseText.replace(regex, (match, date) => releaseDates.push(date));
  // console.log(releaseDates);
  return releaseDates;
}

function getZodiacSign(month, day) {
  let sign;
  // let sign_symbol;
  switch (!0) {
    case (month == 3 && day >= 21) || (month === 4 && day <= 19):
      sign = 'Aries';
      // sign_symbol = '♈︎';
      break;
    case (month == 4 && day >= 20) || (month === 5 && day <= 20):
      sign = 'Taurus';
      // sign_symbol = '♉︎';
      break;
    case (month == 5 && day >= 21) || (month === 6 && day <= 20):
      sign = 'Gemini';
      // sign_symbol = '♊︎';
      break;
    case (month == 6 && day >= 21) || (month === 7 && day <= 22):
      sign = 'Cancer';
      // sign_symbol = '♋︎';
      break;
    case (month == 7 && day >= 23) || (month === 8 && day <= 22):
      sign = 'Leo';
      // sign_symbol = '♌︎';
      break;
    case (month == 8 && day >= 23) || (month === 9 && day <= 22):
      sign = 'Virgo';
      // sign_symbol = '♍︎';
      break;
    case (month == 9 && day >= 23) || (month === 10 && day <= 22):
      sign = 'Libra';
      // sign_symbol = '♎︎';
      break;
    case (month == 10 && day >= 23) || (month === 11 && day <= 21):
      sign = 'Scorpio';
      // sign_symbol = '♏︎';
      break;
    case (month == 11 && day >= 22) || (month === 12 && day <= 21):
      sign = 'Sagittarius';
      // sign_symbol = '♐︎';
      break;
    case (month == 12 && day >= 22) || (month === 1 && day <= 19):
      sign = 'Capricorn';
      // sign_symbol = '♑︎';
      break;
    case (month == 1 && day >= 20) || (month === 2 && day <= 18):
      sign = 'Aquarius';
      // sign_symbol = '♒︎';
      break;
    case (month == 2 && day >= 19) || (month === 3 && day <= 20):
      sign = 'Pisces';
      // sign_symbol = '♓︎';
      break;
  }
  return sign;
}

async function setStorageValue(key, value) {
  await (typeof GM !== 'undefined' ? GM.setValue : GM_setValue)(key, value);
}
async function getStorageValue(key) {
  return await (typeof GM !== 'undefined' ? GM.getValue : GM_getValue)(key);
}
/* async function getStorageKeys() {
	return await (typeof GM !== 'undefined' ? GM.listValues : GM_listValues)();
} */

window.addEventListener('keydown', async ({ key, shiftKey, altKey }) => {
  if (!(key === 'O' && shiftKey && altKey)) return;

  let dialog_confirmation;
  const zodiac_signs_disabled_fresh = (await getStorageValue('zodiac-signs-disabled')) || !1;
  if (zodiac_signs_disabled_fresh === !1) {
    dialog_confirmation = confirm('User script  |  was about that old in that movie\n\nDisable zodiac signs?');
    if (dialog_confirmation) {
      await setStorageValue('zodiac-signs-disabled', !0);
      zodiac_signs_disabled = !0;
    }
  } else {
    dialog_confirmation = confirm('User script  |  was about that old in that movie\n\nEnable zodiac signs?');
    if (dialog_confirmation) {
      await setStorageValue('zodiac-signs-disabled', !1);
      zodiac_signs_disabled = !1;
    }
  }
  // alert(`New options will be applied next time you open the page\n\nzodiac_signs_disabled: ${await getStorageValue('zodiac-signs-disabled')}`);
});

// https://stackoverflow.com/questions/1912501/unescape-html-entities-in-javascript/34064434#34064434
function htmlDecode(input) {
  var doc = new DOMParser().parseFromString(input, 'text/html');
  return doc.documentElement.textContent;
}