Export Youtube Playlist in plaintext

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

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