- // ==UserScript==
- // @name ezClip
- // @namespace Holobox
- // @version 2024-12-05
- // @description ytarchive와 yt-dlp로 유튜브 생방송/아카이브/영상 클립을 따기 쉽게 도와주는 툴
- // @author 물먹는하마
- // @match https://www.youtube.com/*
- // @match https://m.youtube.com/*
- // @run-at document-start
- // @icon https://cutecafe.art/wp-content/uploads/2023/03/fauna01.gif
- // @grant GM_setClipboard
- // @grant GM_addStyle
- // @license MIT
- // ==/UserScript==
-
- let { get: getter, set: setter } = Object.getOwnPropertyDescriptor(
- Object.prototype,
- "playerResponse"
- ) ?? {
- set(e) {
- this[Symbol.for("ezclip")] = e;
- },
- get() {
- return this[Symbol.for("ezclip")];
- },
- },
- isObject = (e) => null != e && "object" == typeof e,
- LEFT =
- (Object.defineProperty(Object.prototype, "playerResponse", {
- set(e) {
- var t, o;
- isObject(e) &&
- (({ streamingData: t, videoDetails: o } = e), isObject(o)) &&
- o.isLive &&
- !o.isLiveDvrEnabled &&
- ((o.isLiveDvrEnabled = !0), isObject(t)) &&
- delete t.serverAbrStreamingUrl,
- setter.call(this, e);
- },
- get() {
- return getter.call(this);
- },
- configurable: !0,
- }),
- -1),
- RIGHT = 1;
- function formatSecondsToHHMMSS(e) {
- var e = Math.floor(e),
- t = Math.floor(e / 3600),
- o = Math.floor((e % 3600) / 60),
- e = e % 60;
- return [
- t.toString().padStart(2, "0"),
- o.toString().padStart(2, "0"),
- e.toString().padStart(2, "0"),
- ].join(":");
- }
- function waitForElm(r) {
- return new Promise((t) => {
- var e = document.querySelector(r);
- if (e) return t(e);
- let o = new MutationObserver((e) => {
- document.querySelector(r) &&
- (t(document.querySelector(r)), o.disconnect());
- });
- o.observe(document, { childList: !0, subtree: !0 });
- });
- }
- async function ytExists(e) {
- e = `https://www.youtube.com/oembed?url=${e}&format=json`;
- try {
- return (await fetch(e)).ok;
- } catch (e) {
- return console.error("Error checking video existence:", e), !1;
- }
- }
- function formatDiffToRelativeTime(e) {
- var t = Math.floor(e / 3600),
- o = Math.floor((e % 3600) / 60),
- e = Math.floor(e % 60);
- return 0 < t ? t + `시간 ${o}분` : 0 < o ? o + `분 ${e}초` : e + "초";
- }
- (async () => {
- let [i, s, e] = await Promise.all([
- waitForElm("#movie_player"),
- waitForElm(".ytp-scrubber-button"),
- waitForElm(".ytp-live-badge"),
- ]);
- var t = document.querySelector(".ytp-progress-list");
- let c = document.createElement("div"),
- a =
- (t.appendChild(c),
- (c.id = "overlay"),
- (c.style.position = "absolute"),
- (c.style.top = "0"),
- (c.style.height = "100%"),
- (c.style.background = "rgba(255, 213, 44, 0.7)"),
- (c.style.zIndex = "1000"),
- (c.style.width = "0"),
- document.createElement("div")),
- d = ((a.id = "wave-container"), i.appendChild(a), console.log(a), null),
- u = null,
- o = Promise.resolve(),
- r = null,
- n = null,
- y = null,
- m = null,
- l;
- function g() {
- var e = document
- .querySelector("iframe#chatframe")
- ?.contentWindow.document.querySelector(
- "div#input.yt-live-chat-text-input-field-renderer"
- ),
- r = e?.parentElement.querySelector(
- "label#label.yt-live-chat-text-input-field-renderer"
- );
- if (
- (y &&
- e &&
- ((n = m() - i.getCurrentTime()) <= 7
- ? (e.setAttribute("contenteditable", "true"),
- (r.textContent = "구독자로 채팅..."))
- : ((n = formatDiffToRelativeTime(n)),
- e.setAttribute("contenteditable", "false"),
- (r.textContent = n + " 전 시점을 시청중입니다"))),
- null === d && null === u)
- )
- (c.style.width = "0"),
- (s.style.background = "var(--yt-spec-static-brand-red,#f03)");
- else {
- e = i.getCurrentTime();
- let t = m(),
- o = 43200 < t ? t - 43200 : 0;
- var [r, n, a] = [l(d), l(u), l(e)];
- function l(e) {
- return null === e ? null : ((e - o) / (t - o)) * 100;
- }
- d && u
- ? ((c.style.width = "auto"),
- (c.style.left = r + "%"),
- (c.style.right = 100 - n + "%"),
- e >= d && e <= u
- ? (s.style.background = "rgba(255, 213, 44, 0.9)")
- : (s.style.background = "var(--yt-spec-static-brand-red,#f03)"),
- (c.style.background = "rgba(255, 213, 44, 0.7)"))
- : d
- ? ((c.style.left = r + "%"),
- (c.style.right = ""),
- e >= d
- ? ((c.style.background = "rgba(255, 213, 44, 0.7)"),
- (c.style.width = "auto"),
- (s.style.background = "rgba(255, 213, 44, 0.9)"),
- (c.style.right = 100 - a + "%"))
- : ((c.style.width = Math.min(20, 100 - r) + "%"),
- (c.style.background =
- "linear-gradient(to right, rgba(255, 213, 44, 0.9), rgba(255, 213, 44, 0))"),
- (s.style.background = "var(--yt-spec-static-brand-red,#f03)")))
- : u &&
- ((c.style.left = ""),
- (c.style.right = 100 - n + "%"),
- e <= u
- ? ((c.style.width = "auto"),
- (c.style.background = "rgba(255, 213, 44, 0.7)"),
- (s.style.background = "rgba(255, 213, 44, 0.9)"),
- (c.style.left = a + "%"))
- : ((c.style.width = Math.min(20, n) + "%"),
- (c.style.background =
- "linear-gradient(to left, rgba(255, 213, 44, 0.9), rgba(255, 213, 44, 0))"),
- (s.style.background = "var(--yt-spec-static-brand-red,#f03)")));
- }
- }
- function v(e, t) {
- var o = t - e,
- r = i.getVideoUrl(),
- o = y
- ? `ytarchive --live-from ${formatSecondsToHHMMSS(
- e
- )} --capture-duration ${formatSecondsToHHMMSS(
- o
- )} --threads 3 ${r} best`
- : `yt-dlp --download-sections "*${formatSecondsToHHMMSS(
- e
- )}-${formatSecondsToHHMMSS(
- t
- )}" --concurrent-fragments 3 --merge-output-format mp4 ` + r;
- GM_setClipboard(o),
- console.log("Command generated and copied to clipboard:", o);
- }
- function f(t = LEFT) {
- if (i.classList.contains("ytp-autohide")) {
- var o = a.offsetHeight,
- r = (3 * o) / 5,
- n = -r / 2 - r / 5;
- let e = document.createElement("div");
- (e.className = "wave"),
- (e.style.width = r + "px"),
- (e.style.height = o + "px"),
- t === LEFT ? (e.style.left = n + "px") : (e.style.right = n + "px"),
- a.appendChild(e),
- e.addEventListener("animationend", () => e.remove());
- }
- }
- (t = new Promise((e) => {
- l = e;
- })),
- i.addEventListener("onStateChange", (e) => {
- o = o.then(() =>
- (async (e) => {
- var t;
- console.log(`state changed: ${r} -> ` + e),
- 1 === e &&
- (t = i.getVideoData())?.video_id &&
- n !== t.video_id &&
- void 0 !== t.isLive &&
- (console.log("video cued"),
- (y = t.isLive),
- (m = await (async (e) => {
- let t = await (async () => {
- let e;
- for (; 0 === (e = i.getCurrentTime()); )
- await new Promise((e) => setTimeout(e, 100));
- return e;
- })(),
- o = new Date().getTime() / 1e3;
- if ((console.log("duration by currentTime: ", t), e))
- return (
- console.log("Load time set: " + o),
- function () {
- var e = new Date().getTime() / 1e3;
- return t + (e - o);
- }
- );
- {
- let e = i.getDuration();
- return function () {
- return e;
- };
- }
- })(y)),
- null === n && l(),
- (n = t.video_id),
- (d = null),
- (u = null),
- g(),
- console.log("videoId: ", n),
- console.log("isLive: ", y)),
- (r = e);
- })(e)
- );
- }),
- o.catch((e) => {
- console.error("Error processing state change:", e);
- }),
- await t,
- setInterval(g, 1e3),
- e.addEventListener("click", g),
- document.addEventListener("mousedown", () => {
- document.addEventListener("mousemove", g);
- }),
- document.addEventListener("mouseup", () => {
- document.removeEventListener("mousemove", g), g();
- }),
- document.addEventListener(
- "keydown",
- function (e) {
- var t;
- document.activeElement.isContentEditable ||
- "INPUT" === document.activeElement.tagName ||
- "TEXTAREA" === document.activeElement.tagName ||
- ((location.href.includes("youtube.com/embed/") ||
- location.href.includes("youtube.com/watch")) &&
- ("KeyE" !== e.code ||
- e.ctrlKey ||
- e.altKey ||
- e.shiftKey ||
- e.metaKey
- ? "KeyR" !== e.code ||
- e.ctrlKey ||
- e.altKey ||
- e.shiftKey ||
- e.metaKey
- ? ("KeyC" !== e.code ||
- e.ctrlKey ||
- e.altKey ||
- e.shiftKey ||
- e.metaKey ||
- (e.preventDefault(),
- e.stopPropagation(),
- (d = null),
- (u = null),
- console.log("Start and end times reset"),
- g()),
- (t = i.getCurrentTime()),
- "KeyD" !== e.code ||
- e.ctrlKey ||
- e.altKey ||
- e.shiftKey ||
- e.metaKey
- ? "KeyF" !== e.code ||
- e.ctrlKey ||
- e.altKey ||
- e.shiftKey ||
- e.metaKey ||
- (e.preventDefault(),
- e.stopPropagation(),
- null !== d && t <= d && (d = null),
- (u = i.getCurrentTime()),
- console.log("End time set to", u),
- g(),
- null !== d && v(d, u),
- f(RIGHT))
- : (e.preventDefault(),
- e.stopPropagation(),
- null !== u && t >= u && (u = null),
- (d = i.getCurrentTime()),
- console.log("Start time set to", d),
- g(),
- null !== u && v(d, u),
- f(LEFT)))
- : (e.preventDefault(),
- e.stopPropagation(),
- y
- ? (i.seekTo(m() + 1e3, !0),
- console.log("Moved to live part of the stream"))
- : null !== u &&
- (i.seekTo(u, !0), console.log("Moved to end time", u)))
- : (e.preventDefault(),
- e.stopPropagation(),
- null !== d &&
- (i.seekTo(d, !0), console.log("Moved to start time", d)))));
- },
- !0
- ),
- GM_addStyle(`
- #wave-container {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
- pointer-events: none; /* 클릭 이벤트 무시 */
- /*background: blue;*/
- /*z-index: 9999;*/
- }
-
- .wave {
- position: absolute;
- left: auto;
- right: auto;
- width: 300px;
- height: 500px;
- background: rgba(255, 222, 5, 0.8); /* wave color - yellow */
- border-radius: 50%;
- transform: scale(0);
- animation: wave 0.6s ease-out forwards;
- z-index: 100;
- }
-
- @keyframes wave {
- 0% {
- transform: scale(0);
- opacity: 1;
- }
- 100% {
- transform: scale(1.2);
- opacity: 0;
- }
- }
- `);
- })();