ezClip

ytarchive와 yt-dlp로 유튜브 생방송/아카이브/영상 클립을 따기 쉽게 도와주는 툴

  1. // ==UserScript==
  2. // @name ezClip
  3. // @namespace Holobox
  4. // @version 2024-12-05
  5. // @description ytarchive와 yt-dlp로 유튜브 생방송/아카이브/영상 클립을 따기 쉽게 도와주는 툴
  6. // @author 물먹는하마
  7. // @match https://www.youtube.com/*
  8. // @match https://m.youtube.com/*
  9. // @run-at document-start
  10. // @icon https://cutecafe.art/wp-content/uploads/2023/03/fauna01.gif
  11. // @grant GM_setClipboard
  12. // @grant GM_addStyle
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. let { get: getter, set: setter } = Object.getOwnPropertyDescriptor(
  17. Object.prototype,
  18. "playerResponse"
  19. ) ?? {
  20. set(e) {
  21. this[Symbol.for("ezclip")] = e;
  22. },
  23. get() {
  24. return this[Symbol.for("ezclip")];
  25. },
  26. },
  27. isObject = (e) => null != e && "object" == typeof e,
  28. LEFT =
  29. (Object.defineProperty(Object.prototype, "playerResponse", {
  30. set(e) {
  31. var t, o;
  32. isObject(e) &&
  33. (({ streamingData: t, videoDetails: o } = e), isObject(o)) &&
  34. o.isLive &&
  35. !o.isLiveDvrEnabled &&
  36. ((o.isLiveDvrEnabled = !0), isObject(t)) &&
  37. delete t.serverAbrStreamingUrl,
  38. setter.call(this, e);
  39. },
  40. get() {
  41. return getter.call(this);
  42. },
  43. configurable: !0,
  44. }),
  45. -1),
  46. RIGHT = 1;
  47. function formatSecondsToHHMMSS(e) {
  48. var e = Math.floor(e),
  49. t = Math.floor(e / 3600),
  50. o = Math.floor((e % 3600) / 60),
  51. e = e % 60;
  52. return [
  53. t.toString().padStart(2, "0"),
  54. o.toString().padStart(2, "0"),
  55. e.toString().padStart(2, "0"),
  56. ].join(":");
  57. }
  58. function waitForElm(r) {
  59. return new Promise((t) => {
  60. var e = document.querySelector(r);
  61. if (e) return t(e);
  62. let o = new MutationObserver((e) => {
  63. document.querySelector(r) &&
  64. (t(document.querySelector(r)), o.disconnect());
  65. });
  66. o.observe(document, { childList: !0, subtree: !0 });
  67. });
  68. }
  69. async function ytExists(e) {
  70. e = `https://www.youtube.com/oembed?url=${e}&format=json`;
  71. try {
  72. return (await fetch(e)).ok;
  73. } catch (e) {
  74. return console.error("Error checking video existence:", e), !1;
  75. }
  76. }
  77. function formatDiffToRelativeTime(e) {
  78. var t = Math.floor(e / 3600),
  79. o = Math.floor((e % 3600) / 60),
  80. e = Math.floor(e % 60);
  81. return 0 < t ? t + `시간 ${o}분` : 0 < o ? o + `분 ${e}초` : e + "초";
  82. }
  83. (async () => {
  84. let [i, s, e] = await Promise.all([
  85. waitForElm("#movie_player"),
  86. waitForElm(".ytp-scrubber-button"),
  87. waitForElm(".ytp-live-badge"),
  88. ]);
  89. var t = document.querySelector(".ytp-progress-list");
  90. let c = document.createElement("div"),
  91. a =
  92. (t.appendChild(c),
  93. (c.id = "overlay"),
  94. (c.style.position = "absolute"),
  95. (c.style.top = "0"),
  96. (c.style.height = "100%"),
  97. (c.style.background = "rgba(255, 213, 44, 0.7)"),
  98. (c.style.zIndex = "1000"),
  99. (c.style.width = "0"),
  100. document.createElement("div")),
  101. d = ((a.id = "wave-container"), i.appendChild(a), console.log(a), null),
  102. u = null,
  103. o = Promise.resolve(),
  104. r = null,
  105. n = null,
  106. y = null,
  107. m = null,
  108. l;
  109. function g() {
  110. var e = document
  111. .querySelector("iframe#chatframe")
  112. ?.contentWindow.document.querySelector(
  113. "div#input.yt-live-chat-text-input-field-renderer"
  114. ),
  115. r = e?.parentElement.querySelector(
  116. "label#label.yt-live-chat-text-input-field-renderer"
  117. );
  118. if (
  119. (y &&
  120. e &&
  121. ((n = m() - i.getCurrentTime()) <= 7
  122. ? (e.setAttribute("contenteditable", "true"),
  123. (r.textContent = "구독자로 채팅..."))
  124. : ((n = formatDiffToRelativeTime(n)),
  125. e.setAttribute("contenteditable", "false"),
  126. (r.textContent = n + " 전 시점을 시청중입니다"))),
  127. null === d && null === u)
  128. )
  129. (c.style.width = "0"),
  130. (s.style.background = "var(--yt-spec-static-brand-red,#f03)");
  131. else {
  132. e = i.getCurrentTime();
  133. let t = m(),
  134. o = 43200 < t ? t - 43200 : 0;
  135. var [r, n, a] = [l(d), l(u), l(e)];
  136. function l(e) {
  137. return null === e ? null : ((e - o) / (t - o)) * 100;
  138. }
  139. d && u
  140. ? ((c.style.width = "auto"),
  141. (c.style.left = r + "%"),
  142. (c.style.right = 100 - n + "%"),
  143. e >= d && e <= u
  144. ? (s.style.background = "rgba(255, 213, 44, 0.9)")
  145. : (s.style.background = "var(--yt-spec-static-brand-red,#f03)"),
  146. (c.style.background = "rgba(255, 213, 44, 0.7)"))
  147. : d
  148. ? ((c.style.left = r + "%"),
  149. (c.style.right = ""),
  150. e >= d
  151. ? ((c.style.background = "rgba(255, 213, 44, 0.7)"),
  152. (c.style.width = "auto"),
  153. (s.style.background = "rgba(255, 213, 44, 0.9)"),
  154. (c.style.right = 100 - a + "%"))
  155. : ((c.style.width = Math.min(20, 100 - r) + "%"),
  156. (c.style.background =
  157. "linear-gradient(to right, rgba(255, 213, 44, 0.9), rgba(255, 213, 44, 0))"),
  158. (s.style.background = "var(--yt-spec-static-brand-red,#f03)")))
  159. : u &&
  160. ((c.style.left = ""),
  161. (c.style.right = 100 - n + "%"),
  162. e <= u
  163. ? ((c.style.width = "auto"),
  164. (c.style.background = "rgba(255, 213, 44, 0.7)"),
  165. (s.style.background = "rgba(255, 213, 44, 0.9)"),
  166. (c.style.left = a + "%"))
  167. : ((c.style.width = Math.min(20, n) + "%"),
  168. (c.style.background =
  169. "linear-gradient(to left, rgba(255, 213, 44, 0.9), rgba(255, 213, 44, 0))"),
  170. (s.style.background = "var(--yt-spec-static-brand-red,#f03)")));
  171. }
  172. }
  173. function v(e, t) {
  174. var o = t - e,
  175. r = i.getVideoUrl(),
  176. o = y
  177. ? `ytarchive --live-from ${formatSecondsToHHMMSS(
  178. e
  179. )} --capture-duration ${formatSecondsToHHMMSS(
  180. o
  181. )} --threads 3 ${r} best`
  182. : `yt-dlp --download-sections "*${formatSecondsToHHMMSS(
  183. e
  184. )}-${formatSecondsToHHMMSS(
  185. t
  186. )}" --concurrent-fragments 3 --merge-output-format mp4 ` + r;
  187. GM_setClipboard(o),
  188. console.log("Command generated and copied to clipboard:", o);
  189. }
  190. function f(t = LEFT) {
  191. if (i.classList.contains("ytp-autohide")) {
  192. var o = a.offsetHeight,
  193. r = (3 * o) / 5,
  194. n = -r / 2 - r / 5;
  195. let e = document.createElement("div");
  196. (e.className = "wave"),
  197. (e.style.width = r + "px"),
  198. (e.style.height = o + "px"),
  199. t === LEFT ? (e.style.left = n + "px") : (e.style.right = n + "px"),
  200. a.appendChild(e),
  201. e.addEventListener("animationend", () => e.remove());
  202. }
  203. }
  204. (t = new Promise((e) => {
  205. l = e;
  206. })),
  207. i.addEventListener("onStateChange", (e) => {
  208. o = o.then(() =>
  209. (async (e) => {
  210. var t;
  211. console.log(`state changed: ${r} -> ` + e),
  212. 1 === e &&
  213. (t = i.getVideoData())?.video_id &&
  214. n !== t.video_id &&
  215. void 0 !== t.isLive &&
  216. (console.log("video cued"),
  217. (y = t.isLive),
  218. (m = await (async (e) => {
  219. let t = await (async () => {
  220. let e;
  221. for (; 0 === (e = i.getCurrentTime()); )
  222. await new Promise((e) => setTimeout(e, 100));
  223. return e;
  224. })(),
  225. o = new Date().getTime() / 1e3;
  226. if ((console.log("duration by currentTime: ", t), e))
  227. return (
  228. console.log("Load time set: " + o),
  229. function () {
  230. var e = new Date().getTime() / 1e3;
  231. return t + (e - o);
  232. }
  233. );
  234. {
  235. let e = i.getDuration();
  236. return function () {
  237. return e;
  238. };
  239. }
  240. })(y)),
  241. null === n && l(),
  242. (n = t.video_id),
  243. (d = null),
  244. (u = null),
  245. g(),
  246. console.log("videoId: ", n),
  247. console.log("isLive: ", y)),
  248. (r = e);
  249. })(e)
  250. );
  251. }),
  252. o.catch((e) => {
  253. console.error("Error processing state change:", e);
  254. }),
  255. await t,
  256. setInterval(g, 1e3),
  257. e.addEventListener("click", g),
  258. document.addEventListener("mousedown", () => {
  259. document.addEventListener("mousemove", g);
  260. }),
  261. document.addEventListener("mouseup", () => {
  262. document.removeEventListener("mousemove", g), g();
  263. }),
  264. document.addEventListener(
  265. "keydown",
  266. function (e) {
  267. var t;
  268. document.activeElement.isContentEditable ||
  269. "INPUT" === document.activeElement.tagName ||
  270. "TEXTAREA" === document.activeElement.tagName ||
  271. ((location.href.includes("youtube.com/embed/") ||
  272. location.href.includes("youtube.com/watch")) &&
  273. ("KeyE" !== e.code ||
  274. e.ctrlKey ||
  275. e.altKey ||
  276. e.shiftKey ||
  277. e.metaKey
  278. ? "KeyR" !== e.code ||
  279. e.ctrlKey ||
  280. e.altKey ||
  281. e.shiftKey ||
  282. e.metaKey
  283. ? ("KeyC" !== e.code ||
  284. e.ctrlKey ||
  285. e.altKey ||
  286. e.shiftKey ||
  287. e.metaKey ||
  288. (e.preventDefault(),
  289. e.stopPropagation(),
  290. (d = null),
  291. (u = null),
  292. console.log("Start and end times reset"),
  293. g()),
  294. (t = i.getCurrentTime()),
  295. "KeyD" !== e.code ||
  296. e.ctrlKey ||
  297. e.altKey ||
  298. e.shiftKey ||
  299. e.metaKey
  300. ? "KeyF" !== e.code ||
  301. e.ctrlKey ||
  302. e.altKey ||
  303. e.shiftKey ||
  304. e.metaKey ||
  305. (e.preventDefault(),
  306. e.stopPropagation(),
  307. null !== d && t <= d && (d = null),
  308. (u = i.getCurrentTime()),
  309. console.log("End time set to", u),
  310. g(),
  311. null !== d && v(d, u),
  312. f(RIGHT))
  313. : (e.preventDefault(),
  314. e.stopPropagation(),
  315. null !== u && t >= u && (u = null),
  316. (d = i.getCurrentTime()),
  317. console.log("Start time set to", d),
  318. g(),
  319. null !== u && v(d, u),
  320. f(LEFT)))
  321. : (e.preventDefault(),
  322. e.stopPropagation(),
  323. y
  324. ? (i.seekTo(m() + 1e3, !0),
  325. console.log("Moved to live part of the stream"))
  326. : null !== u &&
  327. (i.seekTo(u, !0), console.log("Moved to end time", u)))
  328. : (e.preventDefault(),
  329. e.stopPropagation(),
  330. null !== d &&
  331. (i.seekTo(d, !0), console.log("Moved to start time", d)))));
  332. },
  333. !0
  334. ),
  335. GM_addStyle(`
  336. #wave-container {
  337. position: absolute;
  338. top: 0;
  339. left: 0;
  340. width: 100%;
  341. height: 100%;
  342. overflow: hidden;
  343. pointer-events: none; /* 클릭 이벤트 무시 */
  344. /*background: blue;*/
  345. /*z-index: 9999;*/
  346. }
  347. .wave {
  348. position: absolute;
  349. left: auto;
  350. right: auto;
  351. width: 300px;
  352. height: 500px;
  353. background: rgba(255, 222, 5, 0.8); /* wave color - yellow */
  354. border-radius: 50%;
  355. transform: scale(0);
  356. animation: wave 0.6s ease-out forwards;
  357. z-index: 100;
  358. }
  359. @keyframes wave {
  360. 0% {
  361. transform: scale(0);
  362. opacity: 1;
  363. }
  364. 100% {
  365. transform: scale(1.2);
  366. opacity: 0;
  367. }
  368. }
  369. `);
  370. })();