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.4.4/luxon.min.js
// @version        0.18
// @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
  var DATE_PATTERN,
    TIME_PATTERN,
    DATETIME_COMBINE_PATTERN,
    SCHEDULED_LIVESTREAM_START,
    SCHEDULED_PREMIERE_START,
    ONGOING_LIVESTREAM_START;
  var ONGOING_PREMIERE_START,
    ENDED_LIVESTREAM_START,
    ENDED_PREMIERE_START,
    DATETIME_UNTIL_PATTERN,
    SINCE,
    TODAY_AT;
  var YT_API_KEY = "YouTube API-Key";
  var AGE_RESTRICTED = " - FSK 18";
  var SHOW_REFRESH = true;
  var REFRESH_TIMESTAMP = "⟳";
  var SHOW_UNDERLINE_ON_TIMESTAMP = false;
  var BASE_URL =
    "https://www.googleapis.com/youtube/v3/videos?part=snippet,liveStreamingDetails,contentDetails,localizations,player,statistics,status&key=" +
    YT_API_KEY;
  var lang = document.getElementsByTagName("html")[0].getAttribute("lang");
  luxon.Settings.defaultLocale = lang;
  if (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 (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 (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 ";
  }
  var interval = null;
  var changeCheckTimer = null;
  var currentVideoId = null;
  function genUrl() {
    const urlParams = new URLSearchParams(window.location.search);

    if (urlParams.get("v") != null) {
      return BASE_URL + "&id=" + urlParams.get("v");
    } else {
      return "";
    }
  }
  function sleep(milliseconds) {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
  }
  function formatMilliseconds(
    milliseconds,
    joinString,
    showDays,
    showHours,
    showMinutes,
    showSeconds,
    showMilliseconds,
    pad,
    hideDaysOnNull,
  ) {
    let result = "";
    let days = Math.floor(milliseconds / (1000 * 60 * 60 * 24));
    let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
    let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
    let seconds = Math.floor((milliseconds / 1000) % 60);
    milliseconds = milliseconds % 1000;
    if (showDays) {
      if (days < 1 && hideDaysOnNull) {
      } else {
        if (result != "") result += joinString;
        if (pad) {
          if (days < 10) result += "0" + days;
          else result += days;
        } else result += days;
      }
    }
    if (showHours) {
      if (result != "") result += joinString;
      if (pad) result += ("0" + hours).slice(-2);
      else result += hours;
    }
    if (showMinutes) {
      if (result != "") result += joinString;
      if (pad) result += ("0" + minutes).slice(-2);
      else result += minutes;
    }
    if (showSeconds) {
      if (result != "") result += joinString;
      if (pad) result += ("0" + seconds).slice(-2);
      else result += seconds;
    }
    if (showMilliseconds) {
      if (result != "") result += joinString;
      if (pad) result += ("00" + milliseconds).slice(-3);
      else result += milliseconds;
    }
    return result;
  }
  function updateOngoing(startTime) {
    if (interval) {
      clearInterval(interval);
      interval = null;
    }
    interval = setInterval(function () {
      let durationInMilliseconds = luxon.Interval.fromDateTimes(
        startTime,
        luxon.DateTime.now(),
      ).length("milliseconds");
      var ongoingVideoDuration = document.getElementById(
        "ongoing-video-duration",
      );
      ongoingVideoDuration.innerHTML = formatMilliseconds(
        durationInMilliseconds,
        ":",
        true,
        true,
        true,
        true,
        false,
        true,
        true,
      );
      if (ongoingVideoDuration.parentNode) {
        ongoingVideoDuration.parentNode.title =
          ongoingVideoDuration.parentNode.innerText;
      }
    }, 500);
  }
  async function updateLiveContent(premiere, livestream, data, dt) {
    var element = null;
    while (!element) {
      element = document.getElementById("primary-inner");
      await sleep(200);
    }
    var durationInMilliseconds = null;
    var ongoing = false;
    var 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_PATTERN + 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
        dt = luxon.DateTime.fromISO(
          data.items[0].liveStreamingDetails.actualStartTime,
        );
        var endTime = null;
        if (data.items[0].liveStreamingDetails.actualEndTime)
          endTime = luxon.DateTime.fromISO(
            data.items[0].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,
                  ":",
                  true,
                  true,
                  true,
                  true,
                  false,
                  true,
                  true,
                ) +
                "</span>)";
            else if (premiere)
              innerHTML +=
                ONGOING_PREMIERE_START +
                dt.toFormat(TIME_PATTERN) +
                ' (<span id="ongoing-video-duration">' +
                formatMilliseconds(
                  durationInMilliseconds,
                  ":",
                  true,
                  true,
                  true,
                  true,
                  false,
                  true,
                  true,
                ) +
                "</span>)";
            else
              innerHTML +=
                SINCE +
                " " +
                dt.toFormat(TIME_PATTERN) +
                ' (<span id="ongoing-video-duration">' +
                formatMilliseconds(
                  durationInMilliseconds,
                  ":",
                  true,
                  true,
                  true,
                  true,
                  false,
                  true,
                  true,
                ) +
                "</span>)";
          } else {
            if (livestream)
              innerHTML +=
                ONGOING_LIVESTREAM_START +
                dt.toFormat(
                  DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                ) +
                ' (<span id="ongoing-video-duration">' +
                formatMilliseconds(
                  durationInMilliseconds,
                  ":",
                  true,
                  true,
                  true,
                  true,
                  false,
                  true,
                  true,
                ) +
                "</span>)";
            else if (premiere)
              innerHTML +=
                ONGOING_PREMIERE_START +
                dt.toFormat(
                  DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                ) +
                ' (<span id="ongoing-video-duration">' +
                formatMilliseconds(
                  durationInMilliseconds,
                  ":",
                  true,
                  true,
                  true,
                  true,
                  false,
                  true,
                  true,
                ) +
                "</span>)";
            else
              innerHTML +=
                SINCE +
                " " +
                dt.toFormat(
                  DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
                ) +
                ' (<span id="ongoing-video-duration">' +
                formatMilliseconds(
                  durationInMilliseconds,
                  ":",
                  true,
                  true,
                  true,
                  true,
                  false,
                  true,
                  true,
                ) +
                "</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,
                  );
            }
          }
        }
      }
    }
    var 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);
    let 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) {
    var abort = false;
    const processEvent = async () => {
      await sleep(500);
      const urlParams = new URLSearchParams(window.location.search);
      if (urlParams.get("v") != null) {
        let videoId = urlParams.get("v");
        if (videoId == currentVideoId) {
          abort = true;
        } else {
          currentVideoId = videoId;
        }
      }
      if (forceRefresh) abort = false;
      if ((YT_API_KEY != "" || typeof YT_API_KEY != "undefined") && !abort) {
        var url = genUrl();
        if (url != "") {
          fetch(url)
            .then(function (response) {
              return response.json();
            })
            .then(function (data) {
              if (data.pageInfo.totalResults > 0) {
                const addTime = async () => {
                  var dt = luxon.DateTime.fromISO(
                    data.items[0].snippet.publishedAt,
                  );
                  console.log(dt);
                  let payload = {
                    context: {
                      client: {
                        clientName: "WEB",
                        clientVersion: "2.20210614.06.00",
                        originalUrl: window.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,
                  };
                  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(function (response) {
                      return response.text();
                    })
                    .then(function (video_info) {
                      if (interval) {
                        clearInterval(interval);
                        interval = null;
                      }
                      if (changeCheckTimer) {
                        clearInterval(changeCheckTimer);
                        changeCheckTimer = null;
                      }
                      try {
                        /*let player_response = decodeURIComponent(video_info);
                    let urlParams = new URLSearchParams(player_response);
                    if (urlParams.get("player_response") != null) {
                      player_response = urlParams.get("player_response");
                    }
                    player_response = JSON.parse(player_response);// data.items[0].status.privacyStatus = "public" -> Öffentliches Video*/
                        let player_response = JSON.parse(video_info);
                        var premiere =
                          player_response &&
                          !player_response.videoDetails.isLiveContent;
                        premiere =
                          premiere && data.items[0].liveStreamingDetails;
                        var livestream =
                          player_response &&
                          player_response.videoDetails.isLiveContent;
                        var innerHTML =
                          '<span id="dot" class="style-scope ytd-video-primary-info-renderer">•</span>';
                        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);
  }
  //getExactUploadDate();
  //document.addEventListener('click', getExactUploadDate);
  //document.addEventListener('yt-page-data-updated', getExactUploadDate);
  //document.addEventListener('yt-navigate-finish', getExactUploadDate);
  document.addEventListener("refresh-clicked", refreshEventListener);
  //<video style="width: 853px; height: 480px; left: 0px; top: 0px;" tabindex="-1" class="video-stream html5-main-video" src="blob:https://www.youtube.com/0976da77-cfd4-4922-ad9e-383d88a12200"></video>
  /*function main() {
    let videoStream = document.getElementsByClassName('video-stream');
    if (videoStream.length < 1) {
      setTimeout(() => main(), 500);
    } else {
      console.log('video-stream:', videoStream[0]);
      // videoStream[0].addEventListener('loadeddata', (event) => console.log(`Loaded ${event.target.src}`));
      //videoStream[0].addEventListener('loadeddata', (event) => getExactUploadDate());
      videoStream[0].addEventListener('durationchange', (event) => getExactUploadDate());
    }
  }*/
  function main() {
    let ytdPlayer = document.getElementById("ytd-player");
    if (!ytdPlayer) {
      setTimeout(() => main(), 500);
    } else {
      ytdPlayer.addEventListener("yt-player-updated", (event) =>
        getExactUploadDate(),
      );
    }
  }
  main();
  if (new URLSearchParams(window.location.search).get("v") != null) {
    getExactUploadDate();
  }
})();