Youtube exact upload

Adds exact upload time to youtube videos

// ==UserScript==
// @name           Youtube exact upload
// @name:de        YouTube exakter Hochladezeitpunkt
// @description    Adds exact upload time to youtube videos
// @description:de Fügt YouTube-Videos den exakten Hochladezeitpunkt mit Uhrzeit hinzu
// @require        https://cdnjs.cloudflare.com/ajax/libs/luxon/3.5.0/luxon.min.js
// @version        0.19
// @match          https://www.youtube.com/*
// @grant          none
// @namespace      https://greasyfork.org/users/94906
// @license        MIT
// ==/UserScript==

// luxon is for formatting and comparing dates and times

(function () {
  "use strict";
  console.log("YT EXACT UPLOAD LOADED");
  //Pre-Define Variables to prevent warning of redaclaration of variables
  const YT_API_KEY = "YouTube API-Key";
  let DATE_PATTERN,
    TIME_PATTERN,
    DATETIME_COMBINE_PATTERN,
    SCHEDULED_LIVESTREAM_START,
    SCHEDULED_PREMIERE_START,
    ONGOING_LIVESTREAM_START;
  let ONGOING_PREMIERE_START,
    ENDED_LIVESTREAM_START,
    ENDED_PREMIERE_START,
    DATETIME_UNTIL_PATTERN,
    SINCE,
    TODAY_AT;
  const AGE_RESTRICTED = " - FSK 18";
  const SHOW_REFRESH = true;
  const REFRESH_TIMESTAMP = "⟳";
  const SHOW_UNDERLINE_ON_TIMESTAMP = false;
  const BASE_URL =
    "https://www.googleapis.com/youtube/v3/videos?part=snippet,liveStreamingDetails,contentDetails,localizations,player,statistics,status&key=" +
    YT_API_KEY;
  luxon.Settings.defaultLocale = document.documentElement.lang;
  if (document.documentElement.lang.startsWith("de")) {
    DATE_PATTERN = "dd.MM.yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    TIME_PATTERN = "HH:mm:ss 'Uhr'"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    DATETIME_COMBINE_PATTERN = " 'um' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    SCHEDULED_LIVESTREAM_START = "Livestream geplant für: ";
    SCHEDULED_PREMIERE_START = "Premiere geplant für: ";
    ONGOING_LIVESTREAM_START = "Aktiver Livestream seit ";
    ONGOING_PREMIERE_START = "Aktive Premiere seit ";
    ENDED_LIVESTREAM_START = "Livestream von ";
    ENDED_PREMIERE_START = "Premiere von ";
    DATETIME_UNTIL_PATTERN = " bis ";
    SINCE = "Seit";
    TODAY_AT = "Heute um ";
  } else if (document.documentElement.lang.startsWith("fr")) {
    DATE_PATTERN = "dd MMMM yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    TIME_PATTERN = "HH:mm:ss"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    DATETIME_COMBINE_PATTERN = " 'de' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    SCHEDULED_LIVESTREAM_START = "Direct planifié pour le ";
    SCHEDULED_PREMIERE_START = "Première planifiée pour le ";
    ONGOING_LIVESTREAM_START = "Direct en cours depuis ";
    ONGOING_PREMIERE_START = "Première en cours depuis ";
    ENDED_LIVESTREAM_START = "Direct diffusé le ";
    ENDED_PREMIERE_START = "Première diffusée le ";
    DATETIME_UNTIL_PATTERN = " à ";
    SINCE = "Depuis";
    TODAY_AT = "Aujourd'hui à ";
  } else if (document.documentElement.lang.startsWith("it")) {
    DATE_PATTERN = "dd MMMM yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    TIME_PATTERN = "HH:mm:ss"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    DATETIME_COMBINE_PATTERN = " 'alle' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    SCHEDULED_LIVESTREAM_START = "Diretta pianificata per il: ";
    SCHEDULED_PREMIERE_START = "Premiere pianificata per il: ";
    ONGOING_LIVESTREAM_START = "Diretta attiva dalle ";
    ONGOING_PREMIERE_START = "Premiere attiva dalle ";
    ENDED_LIVESTREAM_START = "Diretta del ";
    ENDED_PREMIERE_START = " Premiere del ";
    DATETIME_UNTIL_PATTERN = " fino ";
    SINCE = "Dalle";
    TODAY_AT = "Oggi alle ";
  } else {
    DATE_PATTERN = "dd.MM.yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    TIME_PATTERN = "HH:mm:ss"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    DATETIME_COMBINE_PATTERN = " 'at' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
    SCHEDULED_LIVESTREAM_START = "Livestream scheduled for: ";
    SCHEDULED_PREMIERE_START = "Premiere scheduled for: ";
    ONGOING_LIVESTREAM_START = "Active Livestream since ";
    ONGOING_PREMIERE_START = "Active Premiere since ";
    ENDED_LIVESTREAM_START = "Livestream from ";
    ENDED_PREMIERE_START = "Premiere from ";
    DATETIME_UNTIL_PATTERN = " until ";
    SINCE = "Since";
    TODAY_AT = "Today at ";
  }
  let interval = null;
  let changeCheckTimer = null;
  let currentVideoId = null;
  function getVideoId() {
    return new URLSearchParams(globalThis.location.search).get("v");
  }
  function genUrl() {
    const videoId = getVideoId();
    if (videoId != null) {
      return BASE_URL + "&id=" + videoId;
    } else {
      return "";
    }
  }
  function sleep(milliseconds) {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
  }
  function formatMilliseconds(milliseconds) {
    const dur = luxon.Duration.fromMillis(milliseconds).shiftTo(
      "hours",
      "minutes",
      "seconds",
    );
    return [dur.hours, dur.minutes, dur.seconds].map((unit) =>
      String(unit).padStart(2, "0")
    ).join(":");
  }
  function updateOngoing(startTime) {
    if (interval) {
      clearInterval(interval);
      interval = null;
    }
    interval = setInterval(function () {
      const durationInMilliseconds = luxon.Interval.fromDateTimes(
        startTime,
        luxon.DateTime.now(),
      ).length("milliseconds");
      const ongoingVideoDuration = document.getElementById(
        "ongoing-video-duration",
      );
      ongoingVideoDuration.innerHTML = formatMilliseconds(
        durationInMilliseconds,
      );
      if (ongoingVideoDuration.parentNode) {
        ongoingVideoDuration.parentNode.title =
          ongoingVideoDuration.parentNode.innerText;
      }
    }, 500);
  }
  async function updateLiveContent(premiere, livestream, data, dt) {
    let element = null;
    while (!element) {
      element = document.getElementById("primary-inner");
      await sleep(200);
    }
    let durationInMilliseconds = null;
    let ongoing = false;
    let innerHTML = "";
    if (!premiere && !livestream) {
      // normal video
      if (dt.hasSame(luxon.DateTime.now(), "day")) {
        // today
        innerHTML += `${TODAY_AT}${dt.toFormat(TIME_PATTERN)}`;
      } else {
        innerHTML += dt.toFormat(
          `${DATE_PATTERN}${DATETIME_COMBINE_PATTERN}${TIME_PATTERN}`,
        );
      }
    } else {
      if (!data.items[0].liveStreamingDetails.actualStartTime) {
        // planned
        dt = luxon.DateTime.fromISO(
          data.items[0].liveStreamingDetails.scheduledStartTime,
        );
        if (dt.hasSame(luxon.DateTime.now(), "day")) {
          // today
          if (livestream) {
            innerHTML += `${SCHEDULED_LIVESTREAM_START}${
              dt.toFormat(TIME_PATTERN)
            }`;
          } else if (premiere) {
            innerHTML += `${SCHEDULED_PREMIERE_START}${
              dt.toFormat(TIME_PATTERN)
            }`;
          } else {
            innerHTML += `${TODAY_AT}${dt.toFormat(TIME_PATTERN)}`;
          }
        } else {
          if (livestream) {
            innerHTML += `${SCHEDULED_LIVESTREAM_START}${
              dt.toFormat(
                DATE_PATTERH + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
              )
            }`;
          } else if (premiere) {
            innerHTML += `${SCHEDULED_PREMIERE_START}${
              dt.toFormat(
                DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
              )
            }`;
          } else {
            innerHTML += `${TODAY_AT}${
              dt.toFormat(
                DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
              )
            }`;
          }
        }
      } else {
        // ongoing / ended
        const liveStreamingDetails = data.items[0].liveStreamingDetails;
        dt = luxon.DateTime.fromISO(liveStreamingDetails.actualStartTime);
        let endTime = null;
        if (liveStreamingDetails.actualEndTime) {
          endTime = luxon.DateTime.fromISO(liveStreamingDetails.actualEndTime);
        }
        if (endTime == null) {
          // ongoing
          ongoing = true;
          durationInMilliseconds = luxon.Interval.fromDateTimes(
            dt,
            luxon.DateTime.now(),
          ).length("milliseconds");
          if (dt.hasSame(luxon.DateTime.now(), "day")) {
            // today
            if (livestream) {
              innerHTML += `${ONGOING_LIVESTREAM_START}${
                dt.toFormat(TIME_PATTERN)
              } (<span id="ongoing-video-duration">${
                formatMilliseconds(durationInMilliseconds)
              }</span>)`;
            } else if (premiere) {
              innerHTML += `${ONGOING_PREMIERE_START}${
                dt.toFormat(TIME_PATTERN)
              } (<span id="ongoing-video-duration">${
                formatMilliseconds(durationInMilliseconds)
              }</span>)`;
            } else {
              innerHTML += `${SINCE} ${
                dt.toFormat(TIME_PATTERN)
              } (<span id="ongoing-video-duration">${
                formatMilliseconds(durationInMilliseconds)
              }</span>)`;
            }
          } else {
            if (livestream) {
              innerHTML += `${ONGOING_LIVESTREAM_START}${
                dt.toFormat(
                  DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                )
              } (<span id="ongoing-video-duration">${
                formatMilliseconds(durationInMilliseconds)
              }</span>)`;
            } else if (premiere) {
              innerHTML += `${ONGOING_PREMIERE_START}${
                dt.toFormat(
                  DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                )
              } (<span id="ongoing-video-duration">${
                formatMilliseconds(durationInMilliseconds)
              }</span>)`;
            } else {
              innerHTML += `${SINCE} ${
                dt.toFormat(
                  DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                )
              } (<span id="ongoing-video-duration">${
                formatMilliseconds(durationInMilliseconds)
              }</span>)`;
            }
          }
        } else {
          // ended
          if (dt.hasSame(endTime, "day")) {
            // start date and end date are the same
            if (dt.hasSame(luxon.DateTime.now(), "day")) {
              // today
              if (livestream) {
                innerHTML += `${ENDED_LIVESTREAM_START}${
                  dt.toFormat(TIME_PATTERN)
                }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
              } else if (premiere) {
                innerHTML += `${ENDED_PREMIERE_START}${
                  dt.toFormat(TIME_PATTERN)
                }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
              } else {
                innerHTML += `${TODAY_AT}${dt.toFormat(TIME_PATTERN)}`;
              }
            } else {
              if (livestream) {
                innerHTML += `${ENDED_LIVESTREAM_START}${
                  dt.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
              } else if (premiere) {
                innerHTML += `${ENDED_PREMIERE_START}${
                  dt.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
              } else {
                innerHTML += `${
                  dt.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
              }
            }
          } else {
            if (dt.hasSame(luxon.DateTime.now(), "day")) {
              // today
              if (livestream) {
                innerHTML += `${ENDED_LIVESTREAM_START}${
                  dt.toFormat(TIME_PATTERN)
                }${DATETIME_UNTIL_PATTERN}${
                  endTime.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }`;
              } else if (premiere) {
                innerHTML += `${ENDED_PREMIERE_START}${
                  dt.toFormat(TIME_PATTERN)
                }${DATETIME_UNTIL_PATTERN}${
                  endTime.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }`;
              } else {
                innerHTML += `${TODAY_AT}${
                  dt.toFormat(TIME_PATTERN)
                }${DATETIME_UNTIL_PATTERN}${
                  endTime.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }`;
              }
            } else {
              if (livestream) {
                innerHTML += `${ENDED_LIVESTREAM_START}${
                  dt.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }${DATETIME_UNTIL_PATTERN}${
                  endTime.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }`;
              } else if (premiere) {
                innerHTML += `${ENDED_PREMIERE_START}${
                  dt.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }${DATETIME_UNTIL_PATTERN}${
                  endTime.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }`;
              } else {
                innerHTML += `${
                  dt.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }${DATETIME_UNTIL_PATTERN}${
                  endTime.toFormat(
                    DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                  )
                }`;
              }
            }
          }
        }
      }
    }
    const contentRating = data.items[0].contentDetails.contentRating;
    if (contentRating.ytRating && contentRating.ytRating == "ytAgeRestricted") {
      innerHTML += AGE_RESTRICTED;
    }
    if (SHOW_REFRESH) {
      if (SHOW_UNDERLINE_ON_TIMESTAMP) {
        innerHTML +=
          ' <span id="dot" class="style-scope ytd-video-primary-info-renderer"></span> <span style="color: var(--yt-spec-text-secondary); text-decoration: underline var(--yt-spec-text-secondary); cursor: pointer;" onclick="document.dispatchEvent(new Event(\'refresh-clicked\'));">' +
          REFRESH_TIMESTAMP +
          "</span>";
      } else {
        innerHTML +=
          ' <span id="dot" class="style-scope ytd-video-primary-info-renderer"></span> <span style="color: var(--yt-spec-text-secondary); cursor: pointer;" onclick="document.dispatchEvent(new Event(\'refresh-clicked\'));">' +
          REFRESH_TIMESTAMP +
          "</span>";
      }
    }
    if (ongoing) updateOngoing(dt);
    const primaryInner = document.getElementById("primary-inner");
    let dateTimeValueElem = document.getElementById("exact-date-time");
    if (!dateTimeValueElem) {
      dateTimeValueElem = document.createElement("span");
      dateTimeValueElem.id = "exact-date-time";
      primaryInner.insertBefore(dateTimeValueElem, primaryInner.firstChild);
    }
    dateTimeValueElem.style.color = "white";
    dateTimeValueElem.style.position = "absolute";
    dateTimeValueElem.style.zIndex = "999";
    dateTimeValueElem.innerHTML = innerHTML;
    dateTimeValueElem.title = dateTimeValueElem.innerText;
    return ongoing;
  }
  function getExactUploadDate(forceRefresh = false) {
    let abort = false;
    const processEvent = async () => {
      await sleep(500);
      const videoId = getVideoId();
      if (videoId != null) {
        if (videoId == currentVideoId) {
          abort = true;
        } else {
          currentVideoId = videoId;
        }
      }
      if (forceRefresh) abort = false;
      if ((YT_API_KEY != "" || typeof YT_API_KEY != "undefined") && !abort) {
        const url = genUrl();
        if (url != "") {
          try {
            const data = await fetch(url).then((response) => response.json());
            if (data.pageInfo.totalResults > 0) {
              const addTime = async () => {
                const dt = luxon.DateTime.fromISO(
                  data.items[0].snippet.publishedAt,
                );
                console.log(dt);
                const payload = {
                  context: {
                    client: {
                      clientName: "WEB",
                      clientVersion: "2.20210614.06.00",
                      originalUrl: globalThis.location.href,
                      platform: "DESKTOP",
                      clientFormFactor: "UNKNOWN_FORM_FACTOR",
                      mainAppWebInfo: {
                        graftUrl: "/watch?v=" + currentVideoId,
                        webDisplayMode: "WEB_DISPLAY_MODE_BROWSER",
                        isWebNativeShareAvailable: false,
                      },
                    },
                    user: {
                      lockedSafetyMode: false,
                    },
                    request: {
                      useSsl: true,
                    },
                  },
                  videoId: currentVideoId,
                  racyCheckOk: false,
                  contentCheckOk: false,
                };
                const video_info = await fetch(
                  "https://www.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", /*InnerTube-API-Key*/
                  {
                    method: "POST",
                    headers: {
                      "Content-Type": "application/json",
                    },
                    body: JSON.stringify(payload),
                  },
                ).then((response) => response.json());
                try {
                  if (interval) {
                    clearInterval(interval);
                    interval = null;
                  }
                  if (changeCheckTimer) {
                    clearInterval(changeCheckTimer);
                    changeCheckTimer = null;
                  }
                  try {
                    const premiere = !!(video_info &&
                      !video_info.videoDetails.isLiveContent) &&
                      !!data.items[0].liveStreamingDetails;
                    const livestream = !!(video_info &&
                      video_info.videoDetails.isLiveContent);
                    updateLiveContent(premiere, livestream, data, dt);
                  } catch (ex) {
                    console.error(ex);
                  }
                } catch (error) {
                  console.error(
                    "YOUTUBE EXACT UPLOAD ERROR: " + error,
                    "\nget_video_info doesn't seem to work",
                  );
                }
              };
              addTime();
            }
          } catch (error) {
            console.error(
              "YOUTUBE EXACT UPLOAD ERROR: " + error,
              "\nINVALID API KEY?",
            );
          }
        }
      } else {
        if (!abort) {
          console.error("YOUTUBE EXACT UPLOAD ERROR: Undefined api key");
        }
      }
    };
    processEvent();
  }
  function refreshEventListener() {
    getExactUploadDate(true);
  }
  document.addEventListener("refresh-clicked", refreshEventListener);
  function main() {
    const ytdPlayer = document.getElementById("ytd-player");
    if (!ytdPlayer) {
      setTimeout(() => main(), 500);
    } else {
      ytdPlayer.addEventListener(
        "yt-player-updated",
        () => getExactUploadDate(),
      );
    }
  }
  main();
  if (getVideoId() != null) {
    getExactUploadDate();
  }
})();