您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
显示具体日期而不是“2 星期前”,“1 年前”这种
// ==UserScript== // @name 以绝对时间 (yyyy-mm-dd) 显示 YouTube 的视频上传日期 // @name:en Display YouTube video upload dates as absolute dates (yyyy-mm-dd) // @version 0.5.1 // @description 显示具体日期而不是“2 星期前”,“1 年前”这种 // @description:en Show full upload dates, instead of "1 year ago", "2 weeks ago", etc. // @author InMirrors // @namespace https://greasyfork.org/users/518374 // @match https://www.youtube.com/* // @icon https://www.youtube.com/s/desktop/814d40a6/img/favicon_144x144.png // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function () { 'use strict'; // Convert ISO date string to a localized date string function isoToDate(iso) { let date = new Date(iso); // change the locale to use yyyy-mm-dd format let options = { year: 'numeric', month: '2-digit', day: '2-digit' }; let lang = 'zh-CN'; return date.toLocaleDateString(lang, options).replaceAll('/', '-'); } const PROCESSED_MARKER = '\u200B'; // 零宽空格 (Zero-Width Space) let debugModeEnabled = GM_getValue("debugModeEnabled", false); let debugVidsArrayLength = GM_getValue("debugVidsArrayLength", 4); GM_registerMenuCommand(`Toggle Debug mode ${debugModeEnabled ? "OFF" : "ON"}`, () => { debugModeEnabled = !debugModeEnabled; GM_setValue("debugModeEnabled", debugModeEnabled); alert(`Debug mode is now ${debugModeEnabled ? "ON" : "OFF"}`); }); function setVidsArrayLength() { let input = prompt("Please enter a number, 0 to disable slicing:", debugVidsArrayLength); if (input == null) { // input canceled return; } if (input.trim() !== "" && !isNaN(input)) { // valid input debugVidsArrayLength = Number(input); GM_setValue("debugVidsArrayLength", debugVidsArrayLength); alert("Value updated to: " + debugVidsArrayLength); } else { alert("Invalid input. Please enter a number."); } } if (debugModeEnabled) { GM_registerMenuCommand("Set vids array length", setVidsArrayLength); } function getUploadDate() { let el = document.body.querySelector('player-microformat-renderer script'); if (el) { let parts = el.textContent.split('"startDate":"', 2); if (parts.length == 2) { return parts[1].split('"', 1)[0]; } parts = el.textContent.split('"uploadDate":"', 2); if (parts.length == 2) { return parts[1].split('"', 1)[0]; } } return null; } // Check if the video is a live broadcast function getIsLiveBroadcast() { let el = document.body.querySelector('player-microformat-renderer script'); if (!el) { return null; } let parts = el.textContent.split('"isLiveBroadcast":', 2); if (parts.length != 2) { return false; } let isLiveBroadcast = !!parts[1].split(',', 1)[0]; if (!isLiveBroadcast) { return false; } parts = el.textContent.split('"endDate":"', 2); if (parts.length == 2) { return false; } return true; } // Extract video id from the URL function urlToVideoId(url) { let parts = url.split('/shorts/', 2); if (parts.length === 2) { url = parts[1]; } else { url = parts[0]; } parts = url.split('v=', 2); if (parts.length === 2) { url = parts[1]; } else { url = parts[0]; } return url.split('&', 1)[0]; } // Retrieve the upload date from a remote source using the video id and invoke the callback with the result function getRemoteUploadDate(videoId, callback) { let body = { "context": { "client": { "clientName": "WEB", "clientVersion": "2.20240416.01.00" } }, "videoId": videoId }; fetch('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { let object = data.microformat.playerMicroformatRenderer; if (object.liveBroadcastDetails?.isLiveNow) { callback(object.liveBroadcastDetails.startTimestamp); return; } else if (object.publishDate) { callback(object.publishDate); return; } callback(object.uploadDate); }) .catch(error => { console.error('There was a problem with the fetch operation:', error); }); } /** * Process and update upload date info for a video element * @param {string} videoId - YouTube video ID * @param {HTMLElement} dateElem - DOM element that holds the date * @param {string} originalDateText - The original relative time text */ function fetchAndUpdateUploadDate(videoId, dateElem, originalDateText) { getRemoteUploadDate(videoId, (uploadDate) => { const formattedDate = isoToDate(uploadDate) + PROCESSED_MARKER; let displayText; const oldUploadRegex = /days?|weeks?|months?|years?|天|日|周|週|月|年/; if (!oldUploadRegex.test(originalDateText)) { // Keep original + formatted date for recent uploads displayText = `${originalDateText} · ${formattedDate}`; } else { // Show only formatted date displayText = formattedDate; } dateElem.textContent = displayText; }); } // Update the upload date and display style of video descriptions on the page function startTimers() { /* video page description */ setInterval(() => { // Retrieve the upload date let uploadDate = getUploadDate(); if (!uploadDate) { return; } // Format the date and check if it's a live broadcast uploadDate = isoToDate(uploadDate); let isLiveBroadcast = getIsLiveBroadcast(); if (isLiveBroadcast) { document.body.classList.add('ytud-description-live'); } else { document.body.classList.remove('ytud-description-live'); } // Update the upload date in the video description let el = document.querySelector('#info-container > #info > b'); if (!el) { let span = document.querySelector('#info-container > #info > span:nth-child(1)'); if (!span) { return; } el = document.createElement('b'); el.textContent = uploadDate; span.insertAdjacentElement('afterend', el); } else { if (el.parentNode.children[1] !== el) { let container = el.parentNode; el = container.removeChild(el); container.children[0].insertAdjacentElement('afterend', el); } if (el.firstChild.nodeValue === uploadDate) { return; } el.firstChild.nodeValue = uploadDate; } }, 1000); /** * Finds and processes video elements on the page based on a given configuration. * This function queries for video containers, extracts metadata, * and updates the date information. * @param {object} config - Configuration for a specific type of video list. * @param {string} config.id - Identifier for the configuration. * @param {RegExp} config.urlPattern - A regular expression to test against the current URL. * @param {string} config.videoContainerSelector - CSS selector for the video container elements. * @param {string} config.metaSpansSelector - CSS selector for metadata spans within the video container. * @param {string} config.vidLinkSelector - CSS selector for the video link element. * @param {boolean} config.shouldCreateDateSpan - If a new date span needs to be created. * @param {number} config.insertAfterIndex - Index at which to insert the new date span. * @param {number} config.dateSpanIndex - Index of the date span in the metadata spans list. */ function findAndProcessVids(config) { // Skip when current address does not match the pattern if (config.urlPattern && !config.urlPattern.test(window.location.href)) { return; } let vids = document.querySelectorAll(config.videoContainerSelector); if (vids.length === 0) { // if (debugModeEnabled) console.warn(`No vids found for [${config.id}]`); return; // No videos found for this config, just return. } // Only process some elements to avoid excessive logging in debug mode. if (debugModeEnabled && debugVidsArrayLength != 0 && vids.length > 1) { vids = Array.from(vids).slice(0, debugVidsArrayLength); } vids.forEach((vidContainer) => { const metaSpans = vidContainer.querySelectorAll(config.metaSpansSelector); if (metaSpans.length === 0) { if (debugModeEnabled) console.warn(`No metaSpan found for [${config.id}]`); return; } let dateSpan; // Check if a new date span needs to be created. if (config.shouldCreateDateSpan) { dateSpan = document.createElement('span'); dateSpan.className = 'inline-metadata-item style-scope ytd-video-meta-block ytdf-date'; dateSpan.appendChild(document.createTextNode('')); metaSpans[config.insertAfterIndex].insertAdjacentElement('afterend', dateSpan); } else { dateSpan = metaSpans[config.dateSpanIndex]; } if (!dateSpan) { if (debugModeEnabled) console.warn(`dateSpan is null for [${config.id}]`); return; } const dateText = dateSpan.textContent; if (!dateText) { if (debugModeEnabled) console.warn(`dateText is null for [${config.id}]`); return; } // Skip if already processed. if (dateText.includes(PROCESSED_MARKER)) { return; } // Mark as processed by adding an invisible marker character. dateSpan.textContent = dateText + PROCESSED_MARKER; // Find the video link element to extract the video ID. const vidLinkElem = vidContainer.querySelector(config.vidLinkSelector); if (!vidLinkElem) { if (debugModeEnabled) console.warn(`No vidLinkElem found for [${config.id}]`); return; } const vidLink = vidLinkElem.getAttribute('href'); if (!vidLink) { if (debugModeEnabled) console.warn(`vidLink is null for [${config.id}]`); return; } const videoId = urlToVideoId(vidLink); fetchAndUpdateUploadDate(videoId, dateSpan, dateText); }); } // Configuration array for different video list types. const configs = [ { id: 'Video Page Sidebar', urlPattern: /watch\?v=/, videoContainerSelector: 'yt-lockup-view-model.lockup', metaSpansSelector: '.yt-content-metadata-view-model__delimiter+ .yt-core-attributed-string--link-inherit-color', vidLinkSelector: '.yt-lockup-view-model__content-image', shouldCreateDateSpan: false, dateSpanIndex: 0, }, { id: 'Homepage Videos', urlPattern: /www\.youtube\.com\/?$/, videoContainerSelector: 'ytd-rich-item-renderer.style-scope.ytd-rich-grid-renderer', metaSpansSelector: '.yt-core-attributed-string--link-inherit-color', vidLinkSelector: '.yt-lockup-view-model__content-image', shouldCreateDateSpan: false, dateSpanIndex: 3, }, { id: 'Homepage Shorts', urlPattern: /XXXwww\.youtube\.com\/?$/, // remove XXX to enable this config videoContainerSelector: '', metaSpansSelector: '#metadata-line > span', vidLinkSelector: '.yt-lockup-view-model__content-image', shouldCreateDateSpan: true, insertAfterIndex: 0, dateSpanIndex: 1, }, { id: 'Search List Videos', urlPattern: /results\?search_query=/, videoContainerSelector: 'ytd-video-renderer.ytd-item-section-renderer', metaSpansSelector: '.inline-metadata-item', vidLinkSelector: '#thumbnail', shouldCreateDateSpan: false, dateSpanIndex: 1, }, { id: 'Search List Shorts', urlPattern: /XXXresults\?search_query=/, videoContainerSelector: '', metaSpansSelector: '#metadata-line > span', vidLinkSelector: '.yt-lockup-view-model__content-image', shouldCreateDateSpan: true, insertAfterIndex: 0, dateSpanIndex: 1, }, { id: 'Subscriptions', urlPattern: /subscriptions/, videoContainerSelector: 'ytd-rich-grid-media.ytd-rich-item-renderer', metaSpansSelector: '#metadata-line > span', vidLinkSelector: 'h3 > a', shouldCreateDateSpan: false, dateSpanIndex: 1, }, { id: 'Channel Videos', urlPattern: /www.youtube.com\/@.+\/videos/, videoContainerSelector: 'ytd-rich-grid-media.ytd-rich-item-renderer', metaSpansSelector: '#metadata-line > span', vidLinkSelector: 'h3 > a', shouldCreateDateSpan: false, dateSpanIndex: 1, }, { id: 'Channel Featured Videos', urlPattern: /www.youtube.com\/@.+(\/featured)?/, videoContainerSelector: 'ytd-grid-video-renderer.yt-horizontal-list-renderer', metaSpansSelector: '#metadata-line > span', vidLinkSelector: 'a#thumbnail', shouldCreateDateSpan: false, dateSpanIndex: 1, }, { id: 'Channel For You Videos', urlPattern: /www.youtube.com\/@.+\/?$/, videoContainerSelector: 'ytd-channel-video-player-renderer.ytd-item-section-renderer', metaSpansSelector: '#metadata-line > span', vidLinkSelector: '#title a', shouldCreateDateSpan: false, dateSpanIndex: 1, }, { id: 'Video Playlist', urlPattern: /playlist\?list=/, videoContainerSelector: 'ytd-playlist-video-renderer.ytd-playlist-video-list-renderer', metaSpansSelector: 'span.yt-formatted-string', vidLinkSelector: 'a#thumbnail', shouldCreateDateSpan: false, dateSpanIndex: 2, } ]; // Set up timers for each configuration. configs.forEach(config => { setInterval(() => findAndProcessVids(config), 1000); }); // This section for the topic sidebar is too different and is kept separate. /* search list - topic in sidebar */ setInterval(() => { let vids = document.querySelectorAll('#contents > ytd-universal-watch-card-renderer > #sections > ytd-watch-card-section-sequence-renderer > #lists > ytd-vertical-watch-card-list-renderer > #items > ytd-watch-card-compact-video-renderer'); if (vids.length === 0) { return; } vids.forEach((el) => { let holders = el.querySelectorAll('div.text-wrapper > yt-formatted-string.subtitle'); if (holders.length === 0) { return; } let holder = holders[0]; let separator = ' • '; let parts = holder.firstChild.nodeValue.split(separator, 2); if (parts.length < 2) { return; } let prefix = parts[0] + separator; let dateText = parts[1]; let text = el.getAttribute('date-text'); if (text !== null && text === dateText) { return; } el.setAttribute('date-text', dateText); let link = el.querySelector('a#thumbnail').getAttribute('href'); let videoId = urlToVideoId(link); fetchAndUpdateUploadDate(videoId, holder, el, dateText); }) }, 1000); } startTimers() let styleTag = document.createElement('style'); let cssCode = "#info > span:nth-child(3) {display:none !important;}" + "#info > span:nth-child(4) {display:none !important;}" + "#info > b {font-weight:500 !important;margin-left:6px !important;}" + "#date-text {display:none !important;}" + ".ytud-description-live #info > span:nth-child(1) {display:none !important;}" + ".ytud-description-live #info > b {margin-left:0 !important;margin-right:6px !important;}"; styleTag.textContent = cssCode; document.head.appendChild(styleTag); })();