Youtube exact upload

Adds exact upload time to youtube videos

  1. // ==UserScript==
  2. // @name Youtube exact upload
  3. // @name:de YouTube exakter Hochladezeitpunkt
  4. // @description Adds exact upload time to youtube videos
  5. // @description:de Fügt YouTube-Videos den exakten Hochladezeitpunkt mit Uhrzeit hinzu
  6. // @require https://cdnjs.cloudflare.com/ajax/libs/luxon/3.5.0/luxon.min.js
  7. // @version 0.19
  8. // @match https://www.youtube.com/*
  9. // @grant none
  10. // @namespace https://greasyfork.org/users/94906
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. // luxon is for formatting and comparing dates and times
  15.  
  16. (function () {
  17. "use strict";
  18. console.log("YT EXACT UPLOAD LOADED");
  19. //Pre-Define Variables to prevent warning of redaclaration of variables
  20. const YT_API_KEY = "YouTube API-Key";
  21. let DATE_PATTERN,
  22. TIME_PATTERN,
  23. DATETIME_COMBINE_PATTERN,
  24. SCHEDULED_LIVESTREAM_START,
  25. SCHEDULED_PREMIERE_START,
  26. ONGOING_LIVESTREAM_START;
  27. let ONGOING_PREMIERE_START,
  28. ENDED_LIVESTREAM_START,
  29. ENDED_PREMIERE_START,
  30. DATETIME_UNTIL_PATTERN,
  31. SINCE,
  32. TODAY_AT;
  33. const AGE_RESTRICTED = " - FSK 18";
  34. const SHOW_REFRESH = true;
  35. const REFRESH_TIMESTAMP = "⟳";
  36. const SHOW_UNDERLINE_ON_TIMESTAMP = false;
  37. const BASE_URL =
  38. "https://www.googleapis.com/youtube/v3/videos?part=snippet,liveStreamingDetails,contentDetails,localizations,player,statistics,status&key=" +
  39. YT_API_KEY;
  40. luxon.Settings.defaultLocale = document.documentElement.lang;
  41. if (document.documentElement.lang.startsWith("de")) {
  42. DATE_PATTERN = "dd.MM.yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  43. TIME_PATTERN = "HH:mm:ss 'Uhr'"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  44. DATETIME_COMBINE_PATTERN = " 'um' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  45. SCHEDULED_LIVESTREAM_START = "Livestream geplant für: ";
  46. SCHEDULED_PREMIERE_START = "Premiere geplant für: ";
  47. ONGOING_LIVESTREAM_START = "Aktiver Livestream seit ";
  48. ONGOING_PREMIERE_START = "Aktive Premiere seit ";
  49. ENDED_LIVESTREAM_START = "Livestream von ";
  50. ENDED_PREMIERE_START = "Premiere von ";
  51. DATETIME_UNTIL_PATTERN = " bis ";
  52. SINCE = "Seit";
  53. TODAY_AT = "Heute um ";
  54. } else if (document.documentElement.lang.startsWith("fr")) {
  55. DATE_PATTERN = "dd MMMM yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  56. TIME_PATTERN = "HH:mm:ss"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  57. DATETIME_COMBINE_PATTERN = " 'de' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  58. SCHEDULED_LIVESTREAM_START = "Direct planifié pour le ";
  59. SCHEDULED_PREMIERE_START = "Première planifiée pour le ";
  60. ONGOING_LIVESTREAM_START = "Direct en cours depuis ";
  61. ONGOING_PREMIERE_START = "Première en cours depuis ";
  62. ENDED_LIVESTREAM_START = "Direct diffusé le ";
  63. ENDED_PREMIERE_START = "Première diffusée le ";
  64. DATETIME_UNTIL_PATTERN = " à ";
  65. SINCE = "Depuis";
  66. TODAY_AT = "Aujourd'hui à ";
  67. } else if (document.documentElement.lang.startsWith("it")) {
  68. DATE_PATTERN = "dd MMMM yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  69. TIME_PATTERN = "HH:mm:ss"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  70. DATETIME_COMBINE_PATTERN = " 'alle' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  71. SCHEDULED_LIVESTREAM_START = "Diretta pianificata per il: ";
  72. SCHEDULED_PREMIERE_START = "Premiere pianificata per il: ";
  73. ONGOING_LIVESTREAM_START = "Diretta attiva dalle ";
  74. ONGOING_PREMIERE_START = "Premiere attiva dalle ";
  75. ENDED_LIVESTREAM_START = "Diretta del ";
  76. ENDED_PREMIERE_START = " Premiere del ";
  77. DATETIME_UNTIL_PATTERN = " fino ";
  78. SINCE = "Dalle";
  79. TODAY_AT = "Oggi alle ";
  80. } else {
  81. DATE_PATTERN = "dd.MM.yyyy"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  82. TIME_PATTERN = "HH:mm:ss"; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  83. DATETIME_COMBINE_PATTERN = " 'at' "; // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
  84. SCHEDULED_LIVESTREAM_START = "Livestream scheduled for: ";
  85. SCHEDULED_PREMIERE_START = "Premiere scheduled for: ";
  86. ONGOING_LIVESTREAM_START = "Active Livestream since ";
  87. ONGOING_PREMIERE_START = "Active Premiere since ";
  88. ENDED_LIVESTREAM_START = "Livestream from ";
  89. ENDED_PREMIERE_START = "Premiere from ";
  90. DATETIME_UNTIL_PATTERN = " until ";
  91. SINCE = "Since";
  92. TODAY_AT = "Today at ";
  93. }
  94. let interval = null;
  95. let changeCheckTimer = null;
  96. let currentVideoId = null;
  97. function getVideoId() {
  98. return new URLSearchParams(globalThis.location.search).get("v");
  99. }
  100. function genUrl() {
  101. const videoId = getVideoId();
  102. if (videoId != null) {
  103. return BASE_URL + "&id=" + videoId;
  104. } else {
  105. return "";
  106. }
  107. }
  108. function sleep(milliseconds) {
  109. return new Promise((resolve) => setTimeout(resolve, milliseconds));
  110. }
  111. function formatMilliseconds(milliseconds) {
  112. const dur = luxon.Duration.fromMillis(milliseconds).shiftTo(
  113. "hours",
  114. "minutes",
  115. "seconds",
  116. );
  117. return [dur.hours, dur.minutes, dur.seconds].map((unit) =>
  118. String(unit).padStart(2, "0")
  119. ).join(":");
  120. }
  121. function updateOngoing(startTime) {
  122. if (interval) {
  123. clearInterval(interval);
  124. interval = null;
  125. }
  126. interval = setInterval(function () {
  127. const durationInMilliseconds = luxon.Interval.fromDateTimes(
  128. startTime,
  129. luxon.DateTime.now(),
  130. ).length("milliseconds");
  131. const ongoingVideoDuration = document.getElementById(
  132. "ongoing-video-duration",
  133. );
  134. ongoingVideoDuration.innerHTML = formatMilliseconds(
  135. durationInMilliseconds,
  136. );
  137. if (ongoingVideoDuration.parentNode) {
  138. ongoingVideoDuration.parentNode.title =
  139. ongoingVideoDuration.parentNode.innerText;
  140. }
  141. }, 500);
  142. }
  143. async function updateLiveContent(premiere, livestream, data, dt) {
  144. let element = null;
  145. while (!element) {
  146. element = document.getElementById("primary-inner");
  147. await sleep(200);
  148. }
  149. let durationInMilliseconds = null;
  150. let ongoing = false;
  151. let innerHTML = "";
  152. if (!premiere && !livestream) {
  153. // normal video
  154. if (dt.hasSame(luxon.DateTime.now(), "day")) {
  155. // today
  156. innerHTML += `${TODAY_AT}${dt.toFormat(TIME_PATTERN)}`;
  157. } else {
  158. innerHTML += dt.toFormat(
  159. `${DATE_PATTERN}${DATETIME_COMBINE_PATTERN}${TIME_PATTERN}`,
  160. );
  161. }
  162. } else {
  163. if (!data.items[0].liveStreamingDetails.actualStartTime) {
  164. // planned
  165. dt = luxon.DateTime.fromISO(
  166. data.items[0].liveStreamingDetails.scheduledStartTime,
  167. );
  168. if (dt.hasSame(luxon.DateTime.now(), "day")) {
  169. // today
  170. if (livestream) {
  171. innerHTML += `${SCHEDULED_LIVESTREAM_START}${
  172. dt.toFormat(TIME_PATTERN)
  173. }`;
  174. } else if (premiere) {
  175. innerHTML += `${SCHEDULED_PREMIERE_START}${
  176. dt.toFormat(TIME_PATTERN)
  177. }`;
  178. } else {
  179. innerHTML += `${TODAY_AT}${dt.toFormat(TIME_PATTERN)}`;
  180. }
  181. } else {
  182. if (livestream) {
  183. innerHTML += `${SCHEDULED_LIVESTREAM_START}${
  184. dt.toFormat(
  185. DATE_PATTERH + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  186. )
  187. }`;
  188. } else if (premiere) {
  189. innerHTML += `${SCHEDULED_PREMIERE_START}${
  190. dt.toFormat(
  191. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  192. )
  193. }`;
  194. } else {
  195. innerHTML += `${TODAY_AT}${
  196. dt.toFormat(
  197. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  198. )
  199. }`;
  200. }
  201. }
  202. } else {
  203. // ongoing / ended
  204. const liveStreamingDetails = data.items[0].liveStreamingDetails;
  205. dt = luxon.DateTime.fromISO(liveStreamingDetails.actualStartTime);
  206. let endTime = null;
  207. if (liveStreamingDetails.actualEndTime) {
  208. endTime = luxon.DateTime.fromISO(liveStreamingDetails.actualEndTime);
  209. }
  210. if (endTime == null) {
  211. // ongoing
  212. ongoing = true;
  213. durationInMilliseconds = luxon.Interval.fromDateTimes(
  214. dt,
  215. luxon.DateTime.now(),
  216. ).length("milliseconds");
  217. if (dt.hasSame(luxon.DateTime.now(), "day")) {
  218. // today
  219. if (livestream) {
  220. innerHTML += `${ONGOING_LIVESTREAM_START}${
  221. dt.toFormat(TIME_PATTERN)
  222. } (<span id="ongoing-video-duration">${
  223. formatMilliseconds(durationInMilliseconds)
  224. }</span>)`;
  225. } else if (premiere) {
  226. innerHTML += `${ONGOING_PREMIERE_START}${
  227. dt.toFormat(TIME_PATTERN)
  228. } (<span id="ongoing-video-duration">${
  229. formatMilliseconds(durationInMilliseconds)
  230. }</span>)`;
  231. } else {
  232. innerHTML += `${SINCE} ${
  233. dt.toFormat(TIME_PATTERN)
  234. } (<span id="ongoing-video-duration">${
  235. formatMilliseconds(durationInMilliseconds)
  236. }</span>)`;
  237. }
  238. } else {
  239. if (livestream) {
  240. innerHTML += `${ONGOING_LIVESTREAM_START}${
  241. dt.toFormat(
  242. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  243. )
  244. } (<span id="ongoing-video-duration">${
  245. formatMilliseconds(durationInMilliseconds)
  246. }</span>)`;
  247. } else if (premiere) {
  248. innerHTML += `${ONGOING_PREMIERE_START}${
  249. dt.toFormat(
  250. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  251. )
  252. } (<span id="ongoing-video-duration">${
  253. formatMilliseconds(durationInMilliseconds)
  254. }</span>)`;
  255. } else {
  256. innerHTML += `${SINCE} ${
  257. dt.toFormat(
  258. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  259. )
  260. } (<span id="ongoing-video-duration">${
  261. formatMilliseconds(durationInMilliseconds)
  262. }</span>)`;
  263. }
  264. }
  265. } else {
  266. // ended
  267. if (dt.hasSame(endTime, "day")) {
  268. // start date and end date are the same
  269. if (dt.hasSame(luxon.DateTime.now(), "day")) {
  270. // today
  271. if (livestream) {
  272. innerHTML += `${ENDED_LIVESTREAM_START}${
  273. dt.toFormat(TIME_PATTERN)
  274. }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
  275. } else if (premiere) {
  276. innerHTML += `${ENDED_PREMIERE_START}${
  277. dt.toFormat(TIME_PATTERN)
  278. }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
  279. } else {
  280. innerHTML += `${TODAY_AT}${dt.toFormat(TIME_PATTERN)}`;
  281. }
  282. } else {
  283. if (livestream) {
  284. innerHTML += `${ENDED_LIVESTREAM_START}${
  285. dt.toFormat(
  286. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  287. )
  288. }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
  289. } else if (premiere) {
  290. innerHTML += `${ENDED_PREMIERE_START}${
  291. dt.toFormat(
  292. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  293. )
  294. }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
  295. } else {
  296. innerHTML += `${
  297. dt.toFormat(
  298. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  299. )
  300. }${DATETIME_UNTIL_PATTERN}${endTime.toFormat(TIME_PATTERN)}`;
  301. }
  302. }
  303. } else {
  304. if (dt.hasSame(luxon.DateTime.now(), "day")) {
  305. // today
  306. if (livestream) {
  307. innerHTML += `${ENDED_LIVESTREAM_START}${
  308. dt.toFormat(TIME_PATTERN)
  309. }${DATETIME_UNTIL_PATTERN}${
  310. endTime.toFormat(
  311. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  312. )
  313. }`;
  314. } else if (premiere) {
  315. innerHTML += `${ENDED_PREMIERE_START}${
  316. dt.toFormat(TIME_PATTERN)
  317. }${DATETIME_UNTIL_PATTERN}${
  318. endTime.toFormat(
  319. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  320. )
  321. }`;
  322. } else {
  323. innerHTML += `${TODAY_AT}${
  324. dt.toFormat(TIME_PATTERN)
  325. }${DATETIME_UNTIL_PATTERN}${
  326. endTime.toFormat(
  327. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  328. )
  329. }`;
  330. }
  331. } else {
  332. if (livestream) {
  333. innerHTML += `${ENDED_LIVESTREAM_START}${
  334. dt.toFormat(
  335. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  336. )
  337. }${DATETIME_UNTIL_PATTERN}${
  338. endTime.toFormat(
  339. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  340. )
  341. }`;
  342. } else if (premiere) {
  343. innerHTML += `${ENDED_PREMIERE_START}${
  344. dt.toFormat(
  345. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  346. )
  347. }${DATETIME_UNTIL_PATTERN}${
  348. endTime.toFormat(
  349. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  350. )
  351. }`;
  352. } else {
  353. innerHTML += `${
  354. dt.toFormat(
  355. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  356. )
  357. }${DATETIME_UNTIL_PATTERN}${
  358. endTime.toFormat(
  359. DATE_PATTERN + DATETIME_COMBINE_PATTERN + TIME_PATTERN,
  360. )
  361. }`;
  362. }
  363. }
  364. }
  365. }
  366. }
  367. }
  368. const contentRating = data.items[0].contentDetails.contentRating;
  369. if (contentRating.ytRating && contentRating.ytRating == "ytAgeRestricted") {
  370. innerHTML += AGE_RESTRICTED;
  371. }
  372. if (SHOW_REFRESH) {
  373. if (SHOW_UNDERLINE_ON_TIMESTAMP) {
  374. innerHTML +=
  375. ' <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\'));">' +
  376. REFRESH_TIMESTAMP +
  377. "</span>";
  378. } else {
  379. innerHTML +=
  380. ' <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\'));">' +
  381. REFRESH_TIMESTAMP +
  382. "</span>";
  383. }
  384. }
  385. if (ongoing) updateOngoing(dt);
  386. const primaryInner = document.getElementById("primary-inner");
  387. let dateTimeValueElem = document.getElementById("exact-date-time");
  388. if (!dateTimeValueElem) {
  389. dateTimeValueElem = document.createElement("span");
  390. dateTimeValueElem.id = "exact-date-time";
  391. primaryInner.insertBefore(dateTimeValueElem, primaryInner.firstChild);
  392. }
  393. dateTimeValueElem.style.color = "white";
  394. dateTimeValueElem.style.position = "absolute";
  395. dateTimeValueElem.style.zIndex = "999";
  396. dateTimeValueElem.innerHTML = innerHTML;
  397. dateTimeValueElem.title = dateTimeValueElem.innerText;
  398. return ongoing;
  399. }
  400. function getExactUploadDate(forceRefresh = false) {
  401. let abort = false;
  402. const processEvent = async () => {
  403. await sleep(500);
  404. const videoId = getVideoId();
  405. if (videoId != null) {
  406. if (videoId == currentVideoId) {
  407. abort = true;
  408. } else {
  409. currentVideoId = videoId;
  410. }
  411. }
  412. if (forceRefresh) abort = false;
  413. if ((YT_API_KEY != "" || typeof YT_API_KEY != "undefined") && !abort) {
  414. const url = genUrl();
  415. if (url != "") {
  416. try {
  417. const data = await fetch(url).then((response) => response.json());
  418. if (data.pageInfo.totalResults > 0) {
  419. const addTime = async () => {
  420. const dt = luxon.DateTime.fromISO(
  421. data.items[0].snippet.publishedAt,
  422. );
  423. console.log(dt);
  424. const payload = {
  425. context: {
  426. client: {
  427. clientName: "WEB",
  428. clientVersion: "2.20210614.06.00",
  429. originalUrl: globalThis.location.href,
  430. platform: "DESKTOP",
  431. clientFormFactor: "UNKNOWN_FORM_FACTOR",
  432. mainAppWebInfo: {
  433. graftUrl: "/watch?v=" + currentVideoId,
  434. webDisplayMode: "WEB_DISPLAY_MODE_BROWSER",
  435. isWebNativeShareAvailable: false,
  436. },
  437. },
  438. user: {
  439. lockedSafetyMode: false,
  440. },
  441. request: {
  442. useSsl: true,
  443. },
  444. },
  445. videoId: currentVideoId,
  446. racyCheckOk: false,
  447. contentCheckOk: false,
  448. };
  449. const video_info = await fetch(
  450. "https://www.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", /*InnerTube-API-Key*/
  451. {
  452. method: "POST",
  453. headers: {
  454. "Content-Type": "application/json",
  455. },
  456. body: JSON.stringify(payload),
  457. },
  458. ).then((response) => response.json());
  459. try {
  460. if (interval) {
  461. clearInterval(interval);
  462. interval = null;
  463. }
  464. if (changeCheckTimer) {
  465. clearInterval(changeCheckTimer);
  466. changeCheckTimer = null;
  467. }
  468. try {
  469. const premiere = !!(video_info &&
  470. !video_info.videoDetails.isLiveContent) &&
  471. !!data.items[0].liveStreamingDetails;
  472. const livestream = !!(video_info &&
  473. video_info.videoDetails.isLiveContent);
  474. updateLiveContent(premiere, livestream, data, dt);
  475. } catch (ex) {
  476. console.error(ex);
  477. }
  478. } catch (error) {
  479. console.error(
  480. "YOUTUBE EXACT UPLOAD ERROR: " + error,
  481. "\nget_video_info doesn't seem to work",
  482. );
  483. }
  484. };
  485. addTime();
  486. }
  487. } catch (error) {
  488. console.error(
  489. "YOUTUBE EXACT UPLOAD ERROR: " + error,
  490. "\nINVALID API KEY?",
  491. );
  492. }
  493. }
  494. } else {
  495. if (!abort) {
  496. console.error("YOUTUBE EXACT UPLOAD ERROR: Undefined api key");
  497. }
  498. }
  499. };
  500. processEvent();
  501. }
  502. function refreshEventListener() {
  503. getExactUploadDate(true);
  504. }
  505. document.addEventListener("refresh-clicked", refreshEventListener);
  506. function main() {
  507. const ytdPlayer = document.getElementById("ytd-player");
  508. if (!ytdPlayer) {
  509. setTimeout(() => main(), 500);
  510. } else {
  511. ytdPlayer.addEventListener(
  512. "yt-player-updated",
  513. () => getExactUploadDate(),
  514. );
  515. }
  516. }
  517. main();
  518. if (getVideoId() != null) {
  519. getExactUploadDate();
  520. }
  521. })();