Export Youtube Playlist in plaintext

Shows a list of the playlist video names/channels/URLs in plaintext to be easily copied

当前为 2025-01-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Export Youtube Playlist in plaintext
  3. // @namespace 1N07
  4. // @version 0.9.1
  5. // @description Shows a list of the playlist video names/channels/URLs in plaintext to be easily copied
  6. // @author 1N07
  7. // @license unlicense
  8. // @compatible firefox Likely to work on other userscript managers, but not tested
  9. // @compatible firefox v0.9 Tested on Firefox v134.0.1 and Tampermonkey 5.3.3
  10. // @compatible chrome Likely to work on other userscript managers, but not tested
  11. // @compatible chrome v0.9 Tested on Chrome v132.0.6834.84 and Tampermonkey 5.3.3
  12. // @compatible opera untested, but likely works with at least Tampermonkey
  13. // @compatible edge untested, but likely works with at least Tampermonkey
  14. // @compatible safari untested, but likely works with at least Tampermonkey
  15. // @icon https://www.google.com/s2/favicons?domain=youtube.com
  16. // @match https://www.youtube.com/*
  17. // @grant GM_getValue
  18. // @grant GM_setValue
  19. // @grant GM_addStyle
  20. // ==/UserScript==
  21.  
  22. (() => {
  23. let getVideoTitle = GM_getValue("getVideoTitle", true);
  24. let getVideoChannel = GM_getValue("getVideoChannel", false);
  25. let getVideoURL = GM_getValue("getVideoURL", false);
  26. let videoListSeperator = GM_getValue("videoListSeperator", " ; ");
  27.  
  28. let listCreationAllowed = true;
  29. let urlAtLastCheck = "";
  30. let buttonInsertInterval;
  31.  
  32. //add some CSS
  33. GM_addStyle(`
  34. tp-yt-paper-listbox#items { overflow-x: hidden; }
  35.  
  36. #exportPlainTextList {
  37. cursor: pointer;
  38. height: 36px;
  39. width: 100%;
  40. display: flex;
  41. align-items: center;
  42. }
  43. #exportPlainTextList > img {
  44. height: 24px; width: 24px;
  45. color: rgb(144, 144, 144);
  46. padding: 0 13px 0 16px;
  47. filter: contrast(0%);
  48. }
  49. #exportPlainTextList > span {
  50. font-family: "Roboto","Arial",sans-serif;
  51. color: #d9d9d9;
  52. white-space: nowrap;
  53. font-size: 1.4rem;
  54. line-height: 2rem;
  55. font-weight: 400;
  56. }
  57.  
  58. #exportPlainTextList:hover { background-color: rgba(255,255,255,0.1); }
  59. ytd-menu-popup-renderer.ytd-popup-container { overflow-x: hidden !important; max-height: none !important; }
  60.  
  61. #listDisplayContainer {
  62. position: fixed;
  63. z-index: 9999;
  64. margin: 0 auto;
  65. background-color: #464646;
  66. padding: 10px;
  67. border-radius: 5px;
  68. left: 0;
  69. right: 0;
  70. max-width: 100vw;
  71. width: 1200px;
  72. height: 900px;
  73. max-height: 90vh;
  74. top: 5vh;
  75. resize: both;
  76. overflow: hidden;
  77. }
  78. #listDisplayContainer p {
  79. text-align: center;
  80. }
  81. #listDisplayContainer .title {
  82. font-size: 21px;
  83. font-weight: bold;
  84. color: #d9d9d9;
  85. }
  86. #listDisplayContainer ul {
  87. list-style: none;
  88. font-size: 12px;
  89. scale: 1.4;
  90. color: #d9d9d9;
  91. width: -moz-fit-content;
  92. width: fit-content;
  93. margin: 40px auto;
  94. }
  95. #listDisplayContainer > textarea {
  96. box-sizing: border-box;
  97. width: 100%;
  98. margin: 10px 0;
  99. height: calc(100% - 40px);
  100. background-color: #262626;
  101. border: none;
  102. color: #EEE;
  103. border-radius: 5px;
  104. resize: none;
  105. }
  106. #listDisplayContainer #listDisplayGetListButton {
  107. position: relative;
  108. margin: 10px 0;
  109. font-size: 13px;
  110. left: 50%;
  111. transform: translateX(-50%);
  112. }
  113. #closeTheListThing {
  114. float: right;
  115. font-weight: bold;
  116. background-color: RGBA(255,255,255,0.25);
  117. border: none;
  118. font-size: 17px;
  119. border-radius: 10px;
  120. height: 25px;
  121. width: 25px;
  122. cursor: pointer;
  123. }
  124.  
  125. #closeTheListThing:hover { background-color: rgba(255,255,255,0.5); }
  126.  
  127. tp-yt-iron-dropdown.ytd-popup-container #contentWrapper > yt-sheet-view-model.ytd-popup-container {
  128. max-height: unset !important;
  129. }
  130.  
  131. .yt-pl-export-loading-popup {
  132. position: fixed;
  133. top: 50%;
  134. left: 50%;
  135. transform: translate(-50%, -50%);
  136. background-color: #262626;
  137. padding: 20px;
  138. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  139. z-index: 9999;
  140. border-radius: 8px;
  141. text-align: center;
  142. }
  143. .yt-pl-export-loading-popup-message {
  144. font-size: 2rem;
  145. color: #d9d9d9;
  146. }
  147. `);
  148.  
  149. setInterval(() => {
  150. if (urlAtLastCheck !== window.location.href) {
  151. urlAtLastCheck = window.location.href;
  152. if (urlAtLastCheck.includes("/playlist?list="))
  153. InsertButtonASAP();
  154. else
  155. clearInterval(buttonInsertInterval);
  156. }
  157. }, 100);
  158.  
  159.  
  160. function InsertButtonASAP() {
  161. buttonInsertInterval = setInterval(() => {
  162. //wait for possible previous buttons to stop existing (due to how youtube loads pages) and for the space for the new button to be available
  163. if (!document.getElementById("exportPlainTextList")) {
  164. let place = document.querySelector("tp-yt-iron-dropdown.ytd-popup-container .yt-list-view-model-wiz[role='menu']");
  165. if (!place)
  166. place = document.querySelector("tp-yt-iron-dropdown.ytd-popup-container tp-yt-paper-listbox.ytd-menu-popup-renderer[role='listbox']");
  167. if (place) {
  168. const div = document.createElement('div');
  169. div.id = 'exportPlainTextList';
  170.  
  171. const img = document.createElement('img');
  172. img.src = 'https://i.imgur.com/emlur3a.png';
  173. div.appendChild(img);
  174.  
  175. const span = document.createElement('span');
  176. span.textContent = 'Export Playlist';
  177. div.appendChild(span);
  178.  
  179. place.appendChild(div);
  180.  
  181. div.addEventListener('click', ScrollUntillAllVisible);
  182. }
  183.  
  184. }
  185. }, 100);
  186. }
  187.  
  188. function ScrollUntillAllVisible() {
  189. if (!listCreationAllowed)
  190. return;
  191.  
  192. document.querySelector("ytd-browse[page-subtype='playlist']").click();
  193. const popup = createPopup("Scrolling to load all videos in the playlist. Please wait...");
  194. listCreationAllowed = false;
  195. const scrollInterval = setInterval(() => {
  196. if (document.querySelector("ytd-continuation-item-renderer.ytd-playlist-video-list-renderer")) {
  197. window.scrollTo(0, (document.documentElement || document.body).scrollHeight);
  198. } else {
  199. popup.close();
  200. DisplayListOptions();
  201. clearInterval(scrollInterval);
  202. }
  203. }, 100);
  204. }
  205.  
  206. function DisplayListOptions() {
  207. //#region listDisplayContainer
  208. // Create elements programmatically to avoid inline HTML generation
  209. const container = document.createElement('div');
  210. container.id = 'listDisplayContainer';
  211.  
  212. // Create the content structure
  213. const p = document.createElement('p');
  214. const span = document.createElement('span');
  215. span.classList.add('title');
  216. span.textContent = 'Playlist in plain text';
  217. const closeButton = document.createElement('button');
  218. closeButton.id = 'closeTheListThing';
  219. closeButton.textContent = 'X';
  220.  
  221. p.appendChild(span);
  222. p.appendChild(closeButton);
  223. container.appendChild(p);
  224.  
  225. const textarea = document.createElement('textarea');
  226. textarea.style.display = 'none';
  227. container.appendChild(textarea);
  228.  
  229. const ul = document.createElement('ul');
  230. ul.id = 'listDisplayOptions';
  231.  
  232. // Helper function to create a list item with a checkbox
  233. function createListItem(labelText, checkboxId, checked) {
  234. const li = document.createElement('li');
  235. const label = document.createElement('label');
  236. const input = document.createElement('input');
  237. input.type = 'checkbox';
  238. input.id = checkboxId;
  239. input.name = checkboxId;
  240. input.value = checkboxId;
  241. if (checked) input.checked = true;
  242. label.appendChild(input);
  243. label.appendChild(document.createTextNode(labelText));
  244. li.appendChild(label);
  245. return li;
  246. }
  247.  
  248. // Add list items
  249. ul.appendChild(createListItem('Get titles', 'getVideoTitleCB', getVideoTitle));
  250. ul.appendChild(createListItem('Get channel names', 'getVideoChannelCB', getVideoChannel));
  251. ul.appendChild(createListItem('Get URLs', 'getVideoURLCB', getVideoURL));
  252.  
  253. const nameSeparatorInput = document.createElement('input');
  254. nameSeparatorInput.type = 'text';
  255. nameSeparatorInput.id = 'videoListSeperatorInput';
  256. nameSeparatorInput.name = 'videoListSeperatorInput';
  257. nameSeparatorInput.value = videoListSeperator;
  258. nameSeparatorInput.style.width = '40px';
  259. nameSeparatorInput.style.textAlign = 'center';
  260.  
  261. const nameSeparatorLabel = document.createElement('label');
  262. nameSeparatorLabel.appendChild(nameSeparatorInput);
  263. nameSeparatorLabel.appendChild(document.createTextNode(' Name/Author/URL separator'));
  264.  
  265. const nameSeparatorLi = document.createElement('li');
  266. nameSeparatorLi.appendChild(nameSeparatorLabel);
  267. ul.appendChild(nameSeparatorLi);
  268.  
  269. const getListButton = document.createElement('button');
  270. getListButton.id = 'listDisplayGetListButton';
  271. getListButton.textContent = 'Get list';
  272.  
  273. const buttonLi = document.createElement('li');
  274. buttonLi.appendChild(getListButton);
  275. ul.appendChild(buttonLi);
  276.  
  277. container.appendChild(ul);
  278.  
  279. // Append the container to the body
  280. document.body.appendChild(container);
  281. //#endregion
  282.  
  283. document.getElementById("getVideoTitleCB").addEventListener("change", function () {
  284. getVideoTitle = this.checked;
  285. GM_setValue("getVideoTitle", getVideoTitle);
  286. });
  287. document.getElementById("getVideoChannelCB").addEventListener("change", function () {
  288. getVideoChannel = this.checked;
  289. GM_setValue("getVideoChannel", getVideoChannel);
  290. });
  291. document.getElementById("getVideoURLCB").addEventListener("change", function () {
  292. getVideoURL = this.checked;
  293. GM_setValue("getVideoURL", getVideoURL);
  294. });
  295. document.getElementById("videoListSeperatorInput").addEventListener("change", function () {
  296. videoListSeperator = this.value;
  297. GM_setValue("videoListSeperator", videoListSeperator);
  298. });
  299. document.getElementById("listDisplayGetListButton").addEventListener("click", BuildAndDisplayList);
  300. document.getElementById("closeTheListThing").addEventListener("click", () => {
  301. document.getElementById("listDisplayContainer").remove();
  302. listCreationAllowed = true;
  303. });
  304. }
  305.  
  306. function BuildAndDisplayList() {
  307. document.getElementById("listDisplayOptions").style.display = "none";
  308. document.querySelector("#listDisplayContainer > textarea").style.display = "block";
  309.  
  310. const videoTitleArr = [];
  311. const videoChannelArr = [];
  312. const videoURLArr = [];
  313. let videoCount = 0;
  314.  
  315. for (const element of document.querySelectorAll("ytd-playlist-video-list-renderer > #contents.ytd-playlist-video-list-renderer > ytd-playlist-video-renderer #content")) {
  316. if (getVideoTitle)
  317. videoTitleArr.push(element.querySelector("#video-title").getAttribute("title"));
  318.  
  319. if (getVideoURL)
  320. videoURLArr.push(`https://www.youtube.com${element.querySelector("#video-title").getAttribute("href").split("&")[0]}`);
  321.  
  322. if (getVideoChannel)
  323. videoChannelArr.push(element.querySelector("#channel-name yt-formatted-string.ytd-channel-name > a").textContent);
  324.  
  325. videoCount++;
  326. }
  327.  
  328. let list = "";
  329. for (let i = 0; i < videoCount; i++) {
  330. if (getVideoTitle)
  331. list += videoTitleArr[i];
  332.  
  333. if (getVideoChannel)
  334. list += (getVideoTitle ? `${videoListSeperator}` : "") + videoChannelArr[i];
  335.  
  336. if (getVideoURL)
  337. list += (getVideoTitle || getVideoChannel ? `${videoListSeperator}` : "") + videoURLArr[i];
  338.  
  339. list += "\n";
  340. }
  341.  
  342. document.querySelector("#listDisplayContainer > textarea").value = list;
  343. }
  344.  
  345. function createPopup(message) {
  346. // Create the popup container
  347. const popup = document.createElement('div');
  348. popup.classList.add('yt-pl-export-loading-popup'); // Apply the popup class
  349.  
  350. // Create the message element
  351. const messageElem = document.createElement('p');
  352. messageElem.classList.add("yt-pl-export-loading-popup-message"); // Apply the message class
  353. messageElem.textContent = message;
  354. popup.appendChild(messageElem);
  355.  
  356. // Append the popup to the body
  357. document.body.appendChild(popup);
  358.  
  359. // Return an object that can be used to close the popup later
  360. return {
  361. close: closePopup
  362. };
  363.  
  364. // Function to close the popup
  365. function closePopup() {
  366. document.body.removeChild(popup);
  367. }
  368. }
  369. })();