Last active
February 5, 2026 16:18
-
-
Save kor-bim/b370f378465d487b12216561bda42a51 to your computer and use it in GitHub Desktop.
youtube_upload_date
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name 유튜브 날짜 표시기 | |
| // @name:el YouTube Ημερομηνία βίντεο | |
| // @name:nl YouTube Datumweergave | |
| // @name:nb YouTube Datovisning | |
| // @name:da YouTube Datovisning | |
| // @name:de YouTube Datumsanzeige | |
| // @name:ru YouTube Отображение даты | |
| // @name:ro YouTube Afișare dată | |
| // @name:mr YouTube तारीख दर्शक | |
| // @name:vi YouTube Hiển thị ngày | |
| // @name:be YouTube Адлюстраванне даты | |
| // @name:bg YouTube Показване на дата | |
| // @name:sr YouTube Приказ датума | |
| // @name:sv YouTube Datumvisning | |
| // @name:es YouTube Mostrar fecha | |
| // @name:es-419 YouTube Mostrar fecha | |
| // @name:sk YouTube Zobrazenie dátumu | |
| // @name:ar YouTube عرض التاريخ | |
| // @name:eo YouTube Datmontrado | |
| // @name:en YouTube Date Display | |
| // @name:uk YouTube Відображення дати | |
| // @name:ug YouTube چېسلا كۆرسەتكۈچ | |
| // @name:it YouTube Visualizzazione data | |
| // @name:id YouTube Tampilan tanggal | |
| // @name:ja YouTube 日付表示 | |
| // @name:ka YouTube თარიღის ჩვენება | |
| // @name:zh-CN YouTube 日期显示 | |
| // @name:zh-TW YouTube 日期顯示 | |
| // @name:cs YouTube Zobrazení data | |
| // @name:hr YouTube Prikaz datuma | |
| // @name:th YouTube แสดงวันที่ | |
| // @name:tr YouTube Tarih Gösterimi | |
| // @name:pt-BR YouTube Exibição de data | |
| // @name:pl YouTube Wyświetlanie daty | |
| // @name:fr YouTube Affichage de la date | |
| // @name:fr-CA YouTube Affichage de la date | |
| // @name:fi YouTube Päivämäärän näyttö | |
| // @name:ko 유튜브 날짜 표시기 | |
| // @name:hu YouTube Dátum megjelenítés | |
| // @name:he YouTube הצגת תאריך | |
| // @name:ckb YouTube پیشاندانی بەروار | |
| // @description 썸네일 날짜 영상본문 추천영상 쇼츠 재생목록에 날짜를 표시합니다 | |
| // @description:el Εμφανίζει ημερομηνίες σε μικρογραφίες βίντεο προτάσεις Shorts και λίστες αναπαραγωγής | |
| // @description:nl Toont datums op miniaturen videopagina aanbevelingen Shorts en afspeellijsten | |
| // @description:nb Viser datoer på miniatyrer videosider anbefalinger Shorts og spillelister | |
| // @description:da Viser datoer på miniaturebilleder videosider anbefalinger Shorts og afspilningslister | |
| // @description:de Zeigt Datumsangaben auf Thumbnails Videoseiten Empfehlungen Shorts und Wiedergabelisten an | |
| // @description:ru Отображает даты на превью страницах видео рекомендациях Shorts и плейлистах | |
| // @description:ro Afișează datele pe miniaturi pagini video recomandări Shorts și liste de redare | |
| // @description:mr थंबनेल व्हिडिओ पृष्ठ शिफारसी Shorts आणि प्लेलिस्टवर तारीख दाखवते | |
| // @description:vi Hiển thị ngày trên hình thu nhỏ trang video đề xuất Shorts và danh sách phát | |
| // @description:be Паказвае даты на мініяцюрах старонках відэа рэкамендацыях Shorts і плэйлістах | |
| // @description:bg Показва дати върху миниатюри видео страници препоръки Shorts и плейлисти | |
| // @description:sr Приказује датуме на сличицама видео страницама препорукама Shorts и плејлистама | |
| // @description:sv Visar datum på miniatyrer videosidor rekommendationer Shorts och spellistor | |
| // @description:es Muestra fechas en miniaturas páginas de video recomendaciones Shorts y listas de reproducción | |
| // @description:es-419 Muestra fechas en miniaturas páginas de video recomendaciones Shorts y listas de reproducción | |
| // @description:sk Zobrazuje dátumy na miniatúrach stránkach videí odporúčaniach Shorts a zoznamoch prehrávania | |
| // @description:ar يعرض التواريخ على الصور المصغرة وصفحات الفيديو والمقاطع المقترحة وShorts وقوائم التشغيل | |
| // @description:eo Montras datojn sur bildetoj videosiveloj rekomendoj Shorts kaj ludlistoj | |
| // @description:en Displays dates on thumbnails video pages recommended videos Shorts and playlists | |
| // @description:uk Відображає дати на мініатюрах сторінках відео рекомендаціях Shorts та плейлистах | |
| // @description:ug كىچىك رەسىملەر سىن بەتلىرى تەۋسىيە سىنلار Shorts ۋە قويۇش تىزىملىكىدە چېسلا كۆرسىتىدۇ | |
| // @description:it Mostra le date su miniature pagine video consigliati Shorts e playlist | |
| // @description:id Menampilkan tanggal pada thumbnail halaman video rekomendasi Shorts dan playlist | |
| // @description:ja サムネイル 動画ページ おすすめ Shorts 再生リストに日付を表示します | |
| // @description:ka აჩვენებს თარიღებს მინიატურებზე ვიდეო გვერდებზე რეკომენდაციებზე Shorts და პლეილისტებზე | |
| // @description:zh-CN 在缩略图 视频页面 推荐视频 Shorts 和播放列表中显示日期 | |
| // @description:zh-TW 在縮圖 影片頁面 推薦影片 Shorts 與播放清單中顯示日期 | |
| // @description:cs Zobrazuje data na miniaturách stránkách videí doporučeních Shorts a seznamech skladeb | |
| // @description:hr Prikazuje datume na sličicama stranicama videozapisa preporukama Shorts i popisima za reprodukciju | |
| // @description:th แสดงวันที่บนภาพขนาดย่อ หน้า วิดีโอ วิดีโอแนะนำ Shorts และเพลย์ลิสต์ | |
| // @description:tr Küçük resimlerde video sayfalarında önerilen videolarda Shorts ve oynatma listelerinde tarihleri gösterir | |
| // @description:pt-BR Exibe datas em miniaturas páginas de vídeo recomendações Shorts e playlists | |
| // @description:pl Wyświetla daty na miniaturach stronach wideo poleceniach Shorts i listach odtwarzania | |
| // @description:fr Affiche les dates sur les miniatures pages vidéo recommandations Shorts et playlists | |
| // @description:fr-CA Affiche les dates sur les miniatures pages vidéo recommandations Shorts et playlists | |
| // @description:fi Näyttää päivämäärät pikkukuvissa videosivuilla suosituksissa Shortsissa ja soittolistoissa | |
| // @description:ko 썸네일 날짜 영상본문 추천영상 쇼츠 재생목록 날짜표기 | |
| // @description:hu Dátumokat jelenít meg bélyegképeken videóoldalakon ajánlásokban Shorts és lejátszási listákon | |
| // @description:he מציג תאריכים בתמונות ממוזערות בדפי וידאו בהמלצות ב Shorts וברשימות השמעה | |
| // @description:ckb بەروار لە وێنۆچکەکان پەڕەی ڤیدیۆ پێشنیارەکان Shorts و لیستی لێدان پیشان دەدات | |
| // @author kor-bim | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.2.2 | |
| // @match https://www.youtube.com/* | |
| // @icon https://www.youtube.com/s/desktop/aaaab8bf/img/favicon_144x144.png | |
| // @grant none | |
| // @license MIT | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| const cache = new Map(); // videoId -> formatted date string | |
| const inflight = new Map(); // videoId -> Promise<string|null> | |
| let scanTimeout = null; | |
| // ---------- locale-aware formatter (24h) ---------- | |
| const fmtCache = new Map(); | |
| function getLocales() { | |
| const langs = | |
| Array.isArray(navigator.languages) && navigator.languages.length | |
| ? navigator.languages | |
| : [navigator.language || "en-US"]; | |
| try { | |
| if (typeof window.ytcfg?.get === "function") { | |
| const hl = window.ytcfg.get("HL"); | |
| if (hl) return [hl, ...langs]; | |
| } | |
| } catch (_) {} | |
| return langs; | |
| } | |
| function formatDate24h(dateLike) { | |
| const d = new Date(dateLike); | |
| if (Number.isNaN(d.getTime())) return null; | |
| const locales = getLocales(); | |
| const options = { | |
| year: "numeric", | |
| month: "2-digit", | |
| day: "2-digit", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| hour12: false, | |
| hourCycle: "h23", | |
| }; | |
| const key = `${locales.join(",")}::${JSON.stringify(options)}`; | |
| if (!fmtCache.has(key)) fmtCache.set(key, new Intl.DateTimeFormat(locales, options)); | |
| return fmtCache.get(key).format(d); | |
| } | |
| // ---------- videoId helpers ---------- | |
| function getVideoIdFromHref(href) { | |
| if (!href) return null; | |
| try { | |
| if (href.includes("v=")) return new URL(href, location.origin).searchParams.get("v"); | |
| if (href.includes("/shorts/")) return href.match(/shorts\/([a-zA-Z0-9_-]+)/)?.[1] || null; | |
| } catch (_) {} | |
| return null; | |
| } | |
| function getSafeVideoId(container, selector = "a#thumbnail") { | |
| try { | |
| const anchor = container.querySelector(selector) || container.querySelector("a[href]"); | |
| if (!anchor || !anchor.href) return null; | |
| // Shorts 포함 지원이므로 제외하지 않음 | |
| return getVideoIdFromHref(anchor.href); | |
| } catch (_) {} | |
| return null; | |
| } | |
| function getCurrentShortsIdFromUrl() { | |
| // https://www.youtube.com/shorts/VIDEO_ID | |
| const m = location.pathname.match(/^\/shorts\/([a-zA-Z0-9_-]+)/); | |
| return m?.[1] || null; | |
| } | |
| function getShortsVideoIdFromOverlay(overlayRoot) { | |
| // URL이 /shorts/ID면 그걸 최우선 | |
| const fromUrl = getCurrentShortsIdFromUrl(); | |
| if (fromUrl) return fromUrl; | |
| // 제공 DOM 기준 fallback: /watch?v= 링크 | |
| const a = | |
| overlayRoot.querySelector("yt-reel-multi-format-link-view-model a[href*='watch?v=']") || | |
| overlayRoot.querySelector("a[href*='watch?v=']"); | |
| return a ? getVideoIdFromHref(a.getAttribute("href")) : null; | |
| } | |
| // ---------- fetch date (cache + inflight dedupe) ---------- | |
| async function fetchDate(videoId) { | |
| if (!videoId) return null; | |
| if (cache.has(videoId)) return cache.get(videoId); | |
| if (inflight.has(videoId)) return inflight.get(videoId); | |
| const p = (async () => { | |
| try { | |
| const response = await fetch("https://www.youtube.com/youtubei/v1/player", { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify({ | |
| context: { client: { clientName: "WEB", clientVersion: "2.20250422.01.00" } }, | |
| videoId, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| const micro = data.microformat?.playerMicroformatRenderer; | |
| const date = | |
| micro?.liveBroadcastDetails?.startTimestamp || | |
| micro?.publishDate || | |
| micro?.uploadDate; | |
| const formatted = date ? formatDate24h(date) : null; | |
| if (formatted) cache.set(videoId, formatted); | |
| return formatted; | |
| } catch (_) { | |
| return null; | |
| } finally { | |
| inflight.delete(videoId); | |
| } | |
| })(); | |
| inflight.set(videoId, p); | |
| return p; | |
| } | |
| // ---------- overwrite helper (no append) ---------- | |
| async function overwriteUI(target, videoId) { | |
| if (!target || !videoId) return; | |
| if (target.dataset.ytudProcessing === "true") return; | |
| target.dataset.ytudProcessing = "true"; | |
| try { | |
| const dateStr = await fetchDate(videoId); | |
| if (!dateStr) return; | |
| if (!document.body.contains(target)) return; | |
| if (target.textContent !== dateStr) { | |
| target.textContent = dateStr; | |
| } | |
| } finally { | |
| target.removeAttribute("data-ytud-processing"); | |
| } | |
| } | |
| // ---------- Shorts: stable date label under title (no flicker, no child injection into attributed-string) ---------- | |
| async function upsertShortsDate(overlayRoot) { | |
| const vId = getShortsVideoIdFromOverlay(overlayRoot); | |
| if (!vId) return; | |
| const titleVM = overlayRoot.querySelector("yt-shorts-video-title-view-model"); | |
| if (!titleVM) return; | |
| const titleH2 = titleVM.querySelector("h2"); | |
| if (!titleH2) return; | |
| // create/find a sibling label (NOT inside title span) | |
| let label = titleVM.querySelector(".ytud-shorts-date"); | |
| if (!label) { | |
| label = document.createElement("div"); | |
| label.className = "ytud-shorts-date"; | |
| // YouTube 스타일 최대한 상속 (색/폰트/크기 유사) | |
| label.style.cssText = ` | |
| margin-top: 4px; | |
| font: inherit; | |
| font-size: 14px; | |
| color: inherit; | |
| opacity: 0.9; | |
| white-space: nowrap; | |
| `; | |
| // title 아래에 붙이기 | |
| titleH2.insertAdjacentElement("afterend", label); | |
| } | |
| // 이미 같은 vid 처리면 fetch만(텍스트 업데이트) | |
| if (label.dataset.ytudVid === vId && label.textContent && !label.textContent.includes("…")) { | |
| return; | |
| } | |
| label.dataset.ytudVid = vId; | |
| // placeholder | |
| if (!label.textContent) label.textContent = "…"; | |
| const dateStr = await fetchDate(vId); | |
| if (!dateStr) return; | |
| if (!document.body.contains(label)) return; | |
| if (label.dataset.ytudVid !== vId) return; | |
| label.textContent = dateStr; | |
| } | |
| // ---------- scan ---------- | |
| function runSmartScan() { | |
| // [A] Watch page main date | |
| const mainContainer = document.querySelector("#info.ytd-watch-info-text"); | |
| if (mainContainer) { | |
| const dateTarget = mainContainer.querySelector("span:nth-child(3)"); | |
| const vId = new URLSearchParams(window.location.search).get("v"); | |
| if (dateTarget && vId) overwriteUI(dateTarget, vId); | |
| } | |
| // [B] Playlist | |
| document.querySelectorAll("ytd-playlist-video-renderer").forEach((e) => { | |
| const vId = getSafeVideoId(e); | |
| if (!vId) return; | |
| const metaContainer = e.querySelector("#video-info") || e.querySelector("#metadata-line"); | |
| if (!metaContainer) return; | |
| const spans = metaContainer.querySelectorAll("span"); | |
| const target = spans.length > 0 ? spans[spans.length - 1] : null; | |
| if (target) overwriteUI(target, vId); | |
| }); | |
| // [C] Cards (home/search/recommend) | |
| const cardSelectors = | |
| "ytd-rich-grid-media, ytd-compact-video-renderer, ytd-video-renderer, ytd-grid-video-renderer, yt-lockup-view-model"; | |
| document.querySelectorAll(cardSelectors).forEach((container) => { | |
| const metaLine = | |
| container.querySelector("#metadata-line") || | |
| container.querySelector("yt-content-metadata-view-model"); | |
| if (!metaLine) return; | |
| const spans = metaLine.querySelectorAll("span"); | |
| if (!spans.length) return; | |
| let target = spans.length >= 2 ? spans[1] : spans[0]; | |
| if (container.tagName.toLowerCase() === "yt-lockup-view-model") { | |
| target = spans[spans.length - 1]; | |
| } | |
| const vId = getSafeVideoId(container); | |
| if (target && vId) overwriteUI(target, vId); | |
| }); | |
| // [D] Shorts overlay (add date under title) | |
| document | |
| .querySelectorAll(".metadata-container.ytd-reel-player-overlay-renderer") | |
| .forEach((overlay) => { | |
| upsertShortsDate(overlay); | |
| }); | |
| } | |
| // ---------- observer ---------- | |
| const observer = new MutationObserver(() => { | |
| if (scanTimeout) clearTimeout(scanTimeout); | |
| scanTimeout = setTimeout(runSmartScan, 150); | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| window.addEventListener("yt-navigate-finish", () => { | |
| setTimeout(runSmartScan, 500); | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment