您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show full upload dates in DD/MM/YYYY HH:MMam/pm format with improved performance
当前为
// ==UserScript== // @name Full Date format for Youtube // @version 1.1.2 // @description Show full upload dates in DD/MM/YYYY HH:MMam/pm format with improved performance // @author Ignacio Albiol // @namespace https://greasyfork.org/en/users/1304094 // @match https://www.youtube.com/* // @iconURL https://seekvectors.com/files/download/youtube-icon-yellow-01.jpg // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; const processedVideos = new Map(); const uploadDateCache = new Map(); const apiRequestCache = new Map(); const PROCESS_INTERVAL = 1500; const DEBUG = false; // Set to true to enable debug logging function debugLog(...args) { if (DEBUG) console.log('[YT Date Format]', ...args); } async function getRemoteUploadDate(videoId) { if (!videoId) { debugLog('No video ID provided'); return null; } if (uploadDateCache.has(videoId)) { debugLog('Cache hit for', videoId); return uploadDateCache.get(videoId); } if (apiRequestCache.has(videoId)) { debugLog('Request already in progress for', videoId); return apiRequestCache.get(videoId); } debugLog('Fetching data for', videoId); const requestPromise = (async () => { try { const response = await fetch('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ "context": { "client": { "clientName": "WEB", "clientVersion": "2.20240416.01.00" } }, "videoId": videoId }) }); if (!response.ok) { throw new Error(`Network error: ${response.status}`); } const data = await response.json(); const object = data?.microformat?.playerMicroformatRenderer; const uploadDate = object?.publishDate || object?.uploadDate || object?.liveBroadcastDetails?.startTimestamp || null; if (uploadDate) { debugLog('Found date for', videoId, ':', uploadDate); uploadDateCache.set(videoId, uploadDate); } else { debugLog('No date found for', videoId); } return uploadDate; } catch (error) { console.error('[YT Date Format] Error fetching video data:', error, videoId); return null; } finally { apiRequestCache.delete(videoId); } })(); apiRequestCache.set(videoId, requestPromise); return requestPromise; } function isoToDate(iso) { if (!iso) return ''; try { const date = new Date(iso); if (isNaN(date.getTime())) { debugLog('Invalid date:', iso); return ''; } const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); let hours = date.getHours(); const minutes = String(date.getMinutes()).padStart(2, '0'); const ampm = hours >= 12 ? 'pm' : 'am'; hours = hours % 12 || 12; return `${day}/${month}/${year} ${hours}:${minutes}${ampm}`; } catch (error) { console.error('[YT Date Format] Error formatting date:', error, iso); return ''; } } function urlToVideoId(url) { if (!url) return ''; try { // Handle various YouTube URL formats if (url.includes('/shorts/')) { return url.split('/shorts/')[1].split(/[?#]/)[0]; } if (url.includes('v=')) { return url.split('v=')[1].split(/[?&#]/)[0]; } // Handle direct video IDs (e.g., /watch/VIDEO_ID) if (url.includes('/watch/')) { return url.split('/watch/')[1].split(/[?#]/)[0]; } // Handle URLs with video ID directly in the path const match = url.match(/\/([a-zA-Z0-9_-]{11})(?:[?#]|$)/); if (match) return match[1]; return ''; } catch (error) { console.error('[YT Date Format] Error extracting video ID:', error, url); return ''; } } async function processVideoElement(el, linkSelector, metadataSelector) { try { const metadataLine = el.querySelector(metadataSelector); if (!metadataLine) { debugLog('No metadata line found for selector', metadataSelector); return; } // Find or create the span for holding our date const spanElements = metadataLine.querySelectorAll('span'); let holder; // First, try to find an existing text span (likely the view count span) for (const span of spanElements) { if (span.textContent.includes(' views') || span.textContent.includes(' view') || span.textContent.match(/^\d[\d.,]*\s/)) { holder = span.nextElementSibling; if (!holder) { holder = document.createElement('span'); metadataLine.appendChild(holder); } break; } } // If we couldn't find a suitable span, create one if (!holder) { holder = metadataLine.querySelector('span:nth-child(2)'); if (!holder) { holder = document.createElement('span'); metadataLine.appendChild(holder); } } const linkElement = el.querySelector(linkSelector); if (!linkElement) { debugLog('No link element found for selector', linkSelector); return; } const videoUrl = linkElement.getAttribute('href'); const videoId = urlToVideoId(videoUrl); if (!videoId) { debugLog('Failed to extract video ID from', videoUrl); return; } if (processedVideos.has(videoId)) { debugLog('Video already processed', videoId); return; } debugLog('Processing video', videoId); processedVideos.set(videoId, Date.now()); const uploadDate = await getRemoteUploadDate(videoId); if (uploadDate) { const formattedDate = isoToDate(uploadDate); debugLog('Setting date for', videoId, ':', formattedDate); holder.textContent = formattedDate; holder.style.marginLeft = '4px'; } } catch (error) { console.error('[YT Date Format] Error processing video element:', error); } } function processAllElements() { // Updated selectors for different parts of YouTube const selectors = [ // Related videos in watch page { container: '#items.ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer, #related #items ytd-compact-video-renderer', link: 'a#thumbnail', metadata: '#metadata-line' }, // Videos in home page and channel pages { container: 'ytd-rich-grid-media, ytd-rich-item-renderer, ytd-grid-video-renderer', link: 'a#thumbnail, h3 > a#video-title-link', metadata: '#metadata-line, ytd-video-meta-block #metadata #metadata-line' }, // Search results { container: 'ytd-video-renderer', link: 'a#thumbnail, h3 a#video-title', metadata: '#metadata-line, ytd-video-meta-block #metadata #metadata-line' }, // Channel featured video { container: 'ytd-channel-video-player-renderer', link: 'a, yt-formatted-string > a', metadata: '#metadata-line' } ]; selectors.forEach(({ container, link, metadata }) => { document.querySelectorAll(container).forEach(el => { processVideoElement(el, link, metadata); }); }); // Clean up old processed videos to prevent memory leaks const now = Date.now(); for (const [videoId, timestamp] of processedVideos.entries()) { if (now - timestamp > 10 * 60 * 1000) { processedVideos.delete(videoId); } } } function handleURLChange() { debugLog('URL changed, clearing processed videos cache'); processedVideos.clear(); setTimeout(processAllElements, 1000); } function init() { debugLog('Initializing YouTube Date Format script'); // Add CSS to hide YouTube's default timestamp in some cases const styleTag = document.createElement('style'); styleTag.textContent = ` #info > span:nth-child(3), #info > span:nth-child(4) { display: none !important; } `; document.head.appendChild(styleTag); // Watch for page navigation let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; handleURLChange(); } }).observe(document, { subtree: true, childList: true }); // Also watch title changes (which often indicate page changes in SPAs) const titleEl = document.querySelector('head > title'); if (titleEl) { new MutationObserver(handleURLChange) .observe(titleEl, { childList: true }); } // Process videos periodically setInterval(processAllElements, PROCESS_INTERVAL); // Initial processing setTimeout(processAllElements, 500); setTimeout(processAllElements, 2000); setTimeout(processAllElements, 5000); } // Initialize the script when the page is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();