Export Youtube Playlist in tab delimited text

Creates the current playlist as tab delimited text to be easily copied

  1. // ==UserScript==
  2. // @name Export Youtube Playlist in tab delimited text
  3. // @description Creates the current playlist as tab delimited text to be easily copied
  4. // @author 1N07 & MK
  5. // @namespace max44
  6. // @homepage https://greatest.deepsurf.us/en/users/309172-max44
  7. // @match *://*.youtube.com/*
  8. // @match *://*.youtu.be/*
  9. // @icon https://cdn.icon-icons.com/icons2/1488/PNG/512/5295-youtube-i_102568.png
  10. // @version 2.1.4
  11. // @license MIT
  12. // @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
  13. // @run-at document-idle
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. //Workaround: This document requires 'TrustedHTML' assignment
  20. if (window.trustedTypes && trustedTypes.createPolicy) {
  21. if (!trustedTypes.defaultPolicy) {
  22. const passThroughFn = (x) => x;
  23. trustedTypes.createPolicy('default', {
  24. createHTML: passThroughFn,
  25. createScriptURL: passThroughFn,
  26. createScript: passThroughFn,
  27. });
  28. }
  29. }
  30.  
  31. var listCreationAllowed = true;
  32. var urlAtLastCheck = "";
  33. setInterval(function() {
  34. if (urlAtLastCheck != window.location.href) {
  35. urlAtLastCheck = window.location.href;
  36. if (urlAtLastCheck.includes("/playlist?list=")) InsertButtonASAP();
  37. }
  38. }, 100);
  39.  
  40. function InsertButtonASAP() {
  41. var qRes = document.querySelectorAll("#exportTabTextList"); //Remove previous button
  42. for (let i = 0; i < qRes.length; i++) {
  43. qRes[i].remove();
  44. }
  45.  
  46. let buttonInsertInterval = setInterval(function() {
  47. var htmlButton = "";
  48.  
  49. if (document.querySelectorAll("#exportTabTextList").length == 0) {
  50.  
  51. if (document.querySelectorAll("yt-page-header-renderer:not([hidden]) yt-page-header-view-model yt-flexible-actions-view-model").length > 0) { //New design
  52. qRes = document.querySelectorAll("yt-page-header-renderer:not([hidden]) yt-page-header-view-model yt-flexible-actions-view-model > div.yt-flexible-actions-view-model-wiz__action-row");
  53. if (qRes != null && qRes.length > 0) {
  54. htmlButton = "<button id='exportTabTextList' class='yt-spec-button-shape-next--size-m' style='font-family: Roboto, Arial, sans-serif; font-size: 13px; margin-top: 8px; margin-bottom: 16px; padding-top: 2px; border: none; height: 28px; line-height: normal; opacity: 0.8;'>Export list as tab delimited text</button>";
  55. qRes[0].insertAdjacentHTML("afterend", htmlButton);
  56. }
  57. qRes = document.querySelectorAll("#exportTabTextList");
  58. if (qRes != null && qRes.length > 0) {
  59. qRes[0].onclick = ScrollUntilFullListVisible;
  60. }
  61.  
  62. } else if (document.querySelectorAll("ytd-playlist-header-renderer:not([hidden]) div.ytd-playlist-header-renderer > div.play-menu.ytd-playlist-header-renderer").length > 0) { //Older new design
  63. qRes = document.querySelectorAll("div.metadata-wrapper.ytd-playlist-header-renderer > div.play-menu.ytd-playlist-header-renderer");
  64. if (qRes != null && qRes.length > 0) {
  65. htmlButton = "<button id='exportTabTextList' class='yt-spec-button-shape-next--size-m' style='font-family: Roboto, Arial, sans-serif; font-size: 13px; margin-top: 8px; margin-bottom: 16px; padding-top: 2px; border: none; height: 28px; line-height: normal; opacity: 0.8;'>Export list as tab delimited text</button>";
  66. qRes[0].insertAdjacentHTML("afterend", htmlButton);
  67. }
  68. qRes = document.querySelectorAll("#exportTabTextList");
  69. if (qRes != null && qRes.length > 0) {
  70. qRes[0].onclick = ScrollUntilFullListVisible;
  71. }
  72.  
  73. } else if (document.querySelectorAll("ytd-two-column-browse-results-renderer:not([hidden])[page-subtype='playlist'] div#contents div#header > ytd-sort-filter-header-renderer #filter-menu").length > 0) { //Workaround for very new design - add button above user created playlists
  74. qRes = document.querySelectorAll("ytd-two-column-browse-results-renderer:not([hidden])[page-subtype='playlist'] div#contents div#header > ytd-sort-filter-header-renderer #filter-menu");
  75. if (qRes != null && qRes.length > 0) {
  76. htmlButton = "<button id='exportTabTextList' class='yt-spec-button-shape-next--size-m' style='font-family: Roboto, Arial, sans-serif; font-size: 13px; margin: auto 0 auto 25px; border: none; height: 28px; line-height: normal; opacity: 0.95;'>Export list as tab delimited text</button>";
  77. qRes[0].insertAdjacentHTML("afterend", htmlButton);
  78. }
  79. qRes = document.querySelectorAll("#exportTabTextList");
  80. if (qRes != null && qRes.length > 0) {
  81. qRes[0].onclick = ScrollUntilFullListVisible;
  82. }
  83.  
  84. } else if (document.querySelectorAll("ytd-two-column-browse-results-renderer:not([hidden])[page-subtype='playlist'] div#contents div#header #chips-content #scroll-container").length > 0) { //Workaround for very new design - add button above non-user created playlists
  85. qRes = document.querySelectorAll("ytd-two-column-browse-results-renderer:not([hidden])[page-subtype='playlist'] div#contents div#header #chips-content #scroll-container");
  86. if (qRes != null && qRes.length > 0) {
  87. htmlButton = "<button id='exportTabTextList' class='yt-spec-button-shape-next--size-m' style='font-family: Roboto, Arial, sans-serif; font-size: 13px; margin: auto 0 auto 25px; border: none; height: 28px; line-height: normal; opacity: 0.95;'>Export list as tab delimited text</button>";
  88. qRes[0].insertAdjacentHTML("afterend", htmlButton);
  89. }
  90. qRes = document.querySelectorAll("#exportTabTextList");
  91. if (qRes != null && qRes.length > 0) {
  92. qRes[0].onclick = ScrollUntilFullListVisible;
  93. }
  94.  
  95. } else if (document.querySelectorAll("ytd-playlist-sidebar-renderer:not([hidden]) > ytd-playlist-sidebar-primary-info-renderer.style-scope.ytd-playlist-sidebar-renderer").length > 0) { //Old design
  96. qRes = document.querySelectorAll("ytd-playlist-sidebar-primary-info-renderer.style-scope.ytd-playlist-sidebar-renderer");
  97. if (qRes != null && qRes.length > 0) {
  98. htmlButton = "<button id='exportTabTextList' style='font-family: Roboto, Arial, sans-serif; font-size: 13px; margin: 10px 0px;'>Export list as tab delimited text</button>";
  99. qRes[0].insertAdjacentHTML("afterend", htmlButton);
  100. }
  101. qRes = document.querySelectorAll("#exportTabTextList");
  102. if (qRes != null && qRes.length > 0) {
  103. qRes[0].onclick = ScrollUntilFullListVisible;
  104. }
  105. }
  106.  
  107. //Check whether unavailable videos are hidden or not
  108. //var i;
  109. //var strAux = "";
  110. //var flgHidden = false;
  111. //var myNodeList = document.querySelectorAll("#text");
  112. //for (i = 0; i < myNodeList.length; i++) {
  113. // if (myNodeList[i].className.indexOf("style-scope ytd-alert-with-button-renderer") > -1) {
  114. // strAux = myNodeList[i].innerText;
  115. // strAux = strAux.trim();
  116. // strAux = strAux.toLowerCase();
  117. // if (strAux.indexOf("unavailable videos are hidden") > -1) {
  118. // flgHidden = true;
  119. // break;
  120. // }
  121. // }
  122. //}
  123. //if (flgHidden) {
  124. // $("#exportTabTextList").click(ScrollAsPossible); //Unavailable videos are hidden
  125. //} else {
  126. //$("#exportTabTextList").click(ScrollUntilFullListVisible);
  127. //}
  128. //clearInterval(buttonInsertInterval); - Do not clear interval in order to add button back if playlist is rebuilt
  129. }
  130. }, 100);
  131. }
  132.  
  133. function ScrollUntilFullListVisible() {
  134. if (!listCreationAllowed) return;
  135.  
  136. //Switch focus to playlist
  137. var listOfVideos = document.querySelector("ytd-browse[page-subtype='playlist'] #primary");
  138. if (listOfVideos != null) {
  139. listOfVideos.click();
  140.  
  141. var htmlAlert = "";
  142. listCreationAllowed = false;
  143. var qRes = document.querySelectorAll("#exportTabTextList:not(.yt-spec-button-shape-next--size-m)");
  144. if (qRes != null && qRes.length > 0) {
  145. htmlAlert = `<p id="listBuildMessage" style="color: red; font-size: 1.33em;">Getting full list, please wait...</p>`;
  146. qRes[0].insertAdjacentHTML("afterend", htmlAlert);
  147. }
  148. qRes = document.querySelectorAll("#exportTabTextList.yt-spec-button-shape-next--size-m");
  149. if (qRes != null && qRes.length > 0) {
  150. htmlAlert = `<p id="listBuildMessage" style="color: red; font-size: 1.33em; margin-bottom: 16px; mix-blend-mode: lighten;">Getting full list, please wait...</p>`;
  151. qRes[0].insertAdjacentHTML("afterend", htmlAlert);
  152. }
  153.  
  154. let scrollInterval = setInterval(function(){
  155. if (document.querySelectorAll("ytd-continuation-item-renderer.ytd-playlist-video-list-renderer").length > 0) {
  156. //$(document).scrollTop($(document).height());
  157. document.documentElement.scrollTop = document.documentElement.scrollHeight;
  158. } else {
  159. BuildAndDisplayList();
  160. clearInterval(scrollInterval);
  161. }
  162. }, 100);
  163. }
  164. }
  165.  
  166. /*function ScrollAsPossible() { //If unavailable videos are hidden
  167. if (!listCreationAllowed) return;
  168.  
  169. listCreationAllowed = false;
  170. $("#exportTabTextList").after(`<p id="listBuildMessage" style="color: red; font-size: 1.33em;">Getting full list, please wait...</p>`);
  171. $(document).scrollTop($(document).height());
  172. let scrollInterval2 = setInterval(function(){
  173. if (CheckSpinner()) {
  174. $(document).scrollTop($(document).height());
  175. } else {
  176. BuildAndDisplayList();
  177. clearInterval(scrollInterval2);
  178. }
  179. }, 500);
  180. }
  181.  
  182. function CheckSpinner() { //True if playlist is still loading
  183. var i;
  184. var myNodeList = document.querySelectorAll("#spinner");
  185. for (i = 0; i < myNodeList.length; i++) {
  186. if (myNodeList[i].className.indexOf("style-scope ytd-continuation-item-renderer") > -1) return true;
  187. }
  188. return false;
  189. }*/
  190.  
  191. function BuildAndDisplayList() {
  192.  
  193. var htmlList = "<Name>\t<Channel>\t<Duration>\t<URL>";
  194. var myNodeList = document.querySelectorAll("ytd-playlist-video-renderer.style-scope.ytd-playlist-video-list-renderer:not([hidden])");
  195. //var myNodeList = document.getElementsByTagName("ytd-playlist-video-renderer");
  196. var i;
  197. var myCount = 0;
  198. for (i = 0; i < myNodeList.length; i++) {
  199. //if (myNodeList[i].className.indexOf("style-scope ytd-playlist-video-list-renderer") > -1) {
  200. var mySpanList = myNodeList[i].querySelectorAll("span");
  201. var myAList = myNodeList[i].querySelectorAll("a");
  202. var j;
  203. var strAux = "";
  204. var strAux2 = "";
  205. myCount++;
  206. for (j = 0; j < myAList.length; j++) {
  207. if (myAList[j].id == "video-title") {
  208. strAux = myAList[j].innerText; //Video title
  209. strAux = strAux.replace(/[\x0D\x0A]/g, " ");
  210. htmlList += "\n" + strAux.trim();
  211. strAux2 = myAList[j].href; //Video URL
  212. strAux2 = strAux2.replace(/&list=.*&index=\d+/gi, ""); //Remove reference to list and video's index
  213. strAux2 = strAux2.replace(/&t=.*$/gi, ""); //Remove timestamp
  214. strAux2 = strAux2.replace(/&pp=.*$/gi, ""); //Remove pp parameter
  215. }
  216. }
  217. htmlList += "\t";
  218. for (j = 0; j < myAList.length; j++) {
  219. if (myAList[j].className == "yt-simple-endpoint style-scope yt-formatted-string") {
  220. strAux = myAList[j].innerText; //Channel name
  221. strAux = strAux.replace(/[\x0D\x0A]/g, " ");
  222. htmlList += strAux.trim();
  223. }
  224. }
  225. htmlList += "\t ";
  226. for (j = 0; j < mySpanList.length; j++) {
  227. if (mySpanList[j].className == "style-scope ytd-thumbnail-overlay-time-status-renderer") {
  228. strAux = mySpanList[j].innerText; //Duration
  229. strAux = strAux.replace(/[\x0D\x0A]/g, " ");
  230. htmlList += strAux.trim();
  231. }
  232. }
  233. htmlList += "\t" + strAux2.trim(); //Video URL is the last column
  234. //}
  235. }
  236.  
  237. var qRes = document.querySelectorAll("body");
  238. if (qRes != null && qRes.length > 0) {
  239. htmlList = '<div id="tablistDisplayContainer" style="position: fixed; z-index: 9999; top: 5%; right: 5%; background-color: gray; padding: 10px; border-radius: 5px;"><button id="selectAllAndCopyButton" style="font-family: Roboto, Arial, sans-serif; font-size: 13px;">Select all and copy</button>&nbsp;&nbsp;&nbsp;<button id="closeListButton" style="font-family: Roboto, Arial, sans-serif; font-size: 13px;">Close</button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="font-family: Roboto, Arial, sans-serif; font-size: 13px; font-weight: bold; color: white">Total videos in list: '+myCount+'</span><br><br><textarea id="tabPlayList" style="width: 50vw; height: 80vh; max-width: 90vw; max-height: 90vh;">'+htmlList+'</textarea></div>';
  240. qRes[0].insertAdjacentHTML("afterend", htmlList);
  241. }
  242.  
  243. qRes = document.querySelector("#listBuildMessage");
  244. if (qRes != null) qRes.remove();
  245.  
  246. qRes = document.querySelector("#closeListButton");
  247. if (qRes != null) {
  248. qRes.onclick = function() {
  249. qRes = document.querySelector("#tablistDisplayContainer");
  250. if (qRes != null) qRes.remove();
  251. listCreationAllowed = true;
  252. }
  253. }
  254.  
  255. qRes = document.querySelector("#selectAllAndCopyButton");
  256. if (qRes != null) {
  257. qRes.onclick = function() {
  258. document.getElementById("tabPlayList").select();
  259. document.execCommand("copy");
  260. }
  261. }
  262. }
  263.  
  264. }) ();