Filmot Title Restorer

Restores titles for removed or private videos in YouTube playlists

  1. // ==UserScript==
  2. // @name Filmot Title Restorer
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.34
  5. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
  6. // @description Restores titles for removed or private videos in YouTube playlists
  7. // @author Jopik
  8. // @match https://*.youtube.com/*
  9. // @icon https://www.google.com/s2/favicons?domain=filmot.com
  10. // @grant none
  11. // @require http://code.jquery.com/jquery-3.4.1.min.js
  12. // ==/UserScript==
  13.  
  14. var darkModeBackground="#000099";
  15. var lightModeBackground="#b0f2f4";
  16.  
  17. document.addEventListener('yt-navigate-start', handleNavigateStart);
  18. document.addEventListener('yt-navigate-finish', handleNavigateFinish);
  19. document.addEventListener( 'yt-action', handlePageDataLoad );
  20.  
  21. //fire at least once on load, sometimes handleNavigateFinish on first load yt-navigate-finish already fired before script loads
  22. handleNavigateFinish();
  23.  
  24. function escapeHTML(unsafe) {
  25. return unsafe.replace(
  26. /[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g,
  27. c => '&#' + ('000' + c.charCodeAt(0)).substr(-4, 4) + ';'
  28. )
  29. }
  30.  
  31. function handlePageDataLoad(event){
  32. if (event.detail!=null && event.detail.actionName!=null && event.detail.actionName.indexOf("yt-append-continuation")>=0) {
  33. console.log("filmot yt-append-continuation");
  34. console.log(event);
  35. if (window.location.href.indexOf("/playlist?")>0)
  36. {
  37. extractIDsFullView();
  38. }
  39. }
  40. }
  41.  
  42. function handleNavigateStart(){
  43. var filmotTitles=$(".filmot_title");
  44. filmotTitles.text("");
  45. filmotTitles.removeClass("filmot_title");
  46. var filmotChannels=$(".filmot_channel");
  47. filmotChannels.text("");
  48. filmotChannels.attr("onclick","");
  49. filmotChannels.removeClass("filmot_channel");
  50. cleanUP();
  51. console.log("filmot handleNavigateStart");
  52. }
  53.  
  54. function handleNavigateFinish(){
  55. cleanUP();
  56. console.log("filmot handleNavigateFinish");
  57. if (window.location.href.indexOf("/playlist?")>0)
  58. {
  59. setTimeout(function(){ extractIDsFullView(); }, 500);
  60. }
  61. else
  62. {
  63. setTimeout(function(){ extractIDsSideView(); }, 500);
  64. }
  65. }
  66.  
  67. function cleanUP() {
  68. $(".filmot_hide").show();
  69. $(".filmot_hide").removeClass("filmot_hide");
  70. $(".filmot_newimg").remove();
  71. $(".filmot_highlight").css("background-color","");
  72. $(".filmot_highlight").removeClass("filmot_highlight");
  73. $("#TitleRestoredDiv").remove();
  74. $(".filmot_c_link").remove();
  75. window.RecoveredIDS={};
  76. window.DetectedIDS={};
  77. }
  78.  
  79. function extractIDsFullView() {
  80. window.deletedIDs="";
  81. window.deletedIDCnt=0;
  82. var deletedIDs="";
  83. var deletedIDsCnt=0;
  84. var rendererSelector="a.ytd-playlist-video-renderer";
  85. var a=$(rendererSelector).filter(function() {
  86. return !$(this).attr('aria-label');
  87. }).each(function( index, element ) {
  88. // element == this
  89. var href=$(element).attr('href');
  90. var id=String(href.match(/v=[0-9A-Za-z_\-]*/gm));
  91. id=id.substring(2);
  92. window.DetectedIDS[id]=1;
  93. if (deletedIDs.length>0)
  94. {
  95. deletedIDs+=",";
  96. }
  97. deletedIDs+=id;
  98. deletedIDsCnt++;
  99. });
  100.  
  101. if (deletedIDs.length>0) {
  102. window.deletedIDs=deletedIDs;
  103. window.deletedIDCnt=deletedIDsCnt;
  104. if (document.getElementById ("TitleRestoredBtn")==null)
  105. {
  106. var r= $('<div id="TitleRestoredDiv"><center><button id="TitleRestoredBtn">Restore Titles</button><br><a class="yt-simple-endpoint style-scope yt-formatted-string" href="https://filmot.com" target="_blank">Powered by filmot.com</a></center></div>');
  107. $("#items.ytd-playlist-sidebar-renderer").first().prepend(r);
  108. document.getElementById ("TitleRestoredBtn").addEventListener (
  109. "click", ButtonClickActionFullView, false
  110. );
  111. }
  112. processClick(2,0);
  113. }
  114. }
  115.  
  116.  
  117. function extractIDsSideView() {
  118. window.deletedIDs="";
  119. window.deletedIDCnt=0;
  120. var deletedIDs="";
  121. var deletedIDsCnt=0;
  122. var rendererSelector="a.ytd-playlist-panel-video-renderer";
  123. var a=$(rendererSelector).filter(function() {
  124. return $(this).find("#video-title.ytd-playlist-panel-video-renderer[title='']").length>0;
  125. }).each(function( index, element ) {
  126. // element == this
  127. var href=$(element).attr('href');
  128. var id=String(href.match(/v=[0-9A-Za-z_\-]*/gm));
  129. id=id.substring(2);
  130. window.DetectedIDS[id]=1;
  131. if (deletedIDs.length>0)
  132. {
  133. deletedIDs+=",";
  134. }
  135. deletedIDs+=id;
  136. deletedIDsCnt++;
  137. });
  138.  
  139. if (deletedIDs.length>0) {
  140. window.deletedIDs=deletedIDs;
  141. window.deletedIDCnt=deletedIDsCnt;
  142. if (document.getElementById ("TitleRestoredBtn")==null)
  143. {
  144. var r= $('<div id="TitleRestoredDiv"><center><button id="TitleRestoredBtn">Restore Titles</button><br><a class="yt-simple-endpoint style-scope yt-formatted-string" href="https://filmot.com" target="_blank">Powered by filmot.com</a></center></div>');
  145. $("#container.ytd-playlist-panel-renderer").first().prepend(r);
  146. document.getElementById ("TitleRestoredBtn").addEventListener (
  147. "click", ButtonClickActionSideView, false
  148. );
  149. }
  150. }
  151. }
  152.  
  153. function reportAJAXError(error)
  154. {
  155. alert("Error fetching API results " + error);
  156. }
  157.  
  158. function rgb2lum(rgb)
  159. {
  160. // calculate relative luminance of a color provided by rgb() string
  161. // black is 0, white is 1
  162. rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
  163.  
  164. if (rgb.length==4) {
  165. var R=parseInt(rgb[1],10)/255.0;
  166. var G=parseInt(rgb[2],10)/255.0;
  167. var B=parseInt(rgb[3],10)/255.0;
  168. return 0.2126*R + 0.7152*G + 0.0722*B;
  169. }
  170. return 1;
  171. }
  172.  
  173. function processJSONResultSideView (fetched_details,format)
  174. {
  175. var darkMode=-1;
  176.  
  177. for (let i = 0; i < fetched_details.length; ++i) {
  178. var meta=fetched_details[i];
  179. window.RecoveredIDS[meta.id]=1;
  180. var escapedTitle=escapeHTML(meta.title);
  181. if (meta.channelname==null) {
  182. meta.channelname=fetched_details[i].channelid;
  183. }
  184.  
  185.  
  186. var sel="a.ytd-playlist-panel-video-renderer[href*='"+ meta.id+"']";
  187. var item=$(sel);
  188.  
  189. //console.log(item);
  190.  
  191. item.addClass("filmot_highlight");
  192.  
  193. var titleItem=item.find("#video-title");
  194. titleItem.text(meta.title);
  195. titleItem.attr("title",meta.title);
  196. titleItem.attr("aria-label",meta.title);
  197. titleItem.addClass("filmot_title");
  198.  
  199.  
  200. if (darkMode==-1)
  201. {
  202. var lum=rgb2lum(titleItem.css("color"));
  203. darkMode = (lum>0.51)?1:0; //if text is bright it means we are in dark mode
  204. }
  205.  
  206. item.css("background-color",(darkMode==0)?lightModeBackground:darkModeBackground);
  207.  
  208. var channelItem=item.find("#byline");
  209.  
  210. channelItem.text(fetched_details[i].channelname);
  211. channelItem.attr("onclick","window.open('https://www.youtube.com/channel/" +fetched_details[i].channelid + "', '_blank'); event.stopPropagation(); return false;");
  212. channelItem.addClass("filmot_channel");
  213.  
  214. item.find(".filmot_newimg").remove();
  215. var newThumb='<img id="filmot_newimg" class="style-scope yt-img-shadow filmot_newimg" onclick="prompt(\'Full Title\', \''+ escapedTitle+ '\'); event.stopPropagation(); return false;" title="' + escapedTitle + '" width="100" src="https://filmot.com/vi/' + meta.id + '/default.jpg">';
  216. item.find("yt-img-shadow.ytd-thumbnail").append(newThumb);
  217. item.find("#img.yt-img-shadow").addClass("filmot_hide");
  218. item.find("#img.yt-img-shadow").hide();
  219. }
  220.  
  221. $("#TitleRestoredBtn").text(Object.keys(window.RecoveredIDS).length+ " of " +Object.keys(window.DetectedIDS).length + " restored");
  222. }
  223.  
  224.  
  225. function processJSONResultFullView (fetched_details,format)
  226. {
  227. var darkMode=-1;
  228.  
  229. for (let i = 0; i < fetched_details.length; ++i) {
  230. var meta=fetched_details[i];
  231. window.RecoveredIDS[meta.id]=1;
  232. var escapedTitle=escapeHTML(meta.title);
  233.  
  234. if (meta.channelname==null) {
  235. meta.channelname=fetched_details[i].channelid;
  236. }
  237.  
  238. var rendererSelector="#container.ytd-playlist-video-renderer";
  239. var a=$(rendererSelector).filter(function() {
  240. return $(this).find("a.ytd-playlist-video-renderer[href*='"+ meta.id+"']").length>0;
  241. }).each(function( index, element ) {
  242. // element == this
  243. var item=$(element);
  244.  
  245. item.addClass("filmot_highlight");
  246.  
  247. var titleItem=item.find("#video-title");
  248. titleItem.text(meta.title);
  249. titleItem.attr("title",meta.title);
  250. titleItem.attr("aria-label",meta.title);
  251. titleItem.addClass("filmot_title");
  252.  
  253. if (darkMode==-1)
  254. {
  255. var lum=rgb2lum(titleItem.css("color"));
  256. darkMode = (lum>0.51)?1:0; //if text is bright it means we are in dark mode
  257. }
  258.  
  259. item.css("background-color",(darkMode==0)?lightModeBackground:darkModeBackground);
  260.  
  261. var channelItem=item.find("yt-formatted-string.ytd-channel-name");
  262. //console.log(channelItem);
  263. channelItem.find("a.filmot_c_link").remove();
  264. var channelLink="<a class='filmot_c_link yt-simple-endpoint style-scope yt-formatted-string' dir='auto' href='https://www.youtube.com/channel/" +meta.channelid + "'>" + escapeHTML(meta.channelname) + "</a>";
  265. channelItem.append(channelLink);
  266.  
  267. item.find("#byline-container").attr("hidden",false);
  268. item.find(".filmot_newimg").remove();
  269. var newThumb='<img id="filmot_newimg" class="style-scope yt-img-shadow filmot_newimg" onclick="prompt(\'Full Title\', \''+ escapedTitle+ '\'); event.stopPropagation(); return false;" title="' + escapedTitle + '" width="100" src="https://filmot.com/vi/' + meta.id + '/default.jpg">';
  270. item.find("yt-img-shadow.ytd-thumbnail").append(newThumb);
  271. item.find("#img.yt-img-shadow").addClass("filmot_hide");
  272. item.find("#img.yt-img-shadow").hide();
  273.  
  274. });
  275.  
  276. }
  277.  
  278. $("#TitleRestoredBtn").text(Object.keys(window.RecoveredIDS).length+ " of " + Object.keys(window.DetectedIDS).length + " restored");
  279. }
  280.  
  281. function processClick(format,nTry)
  282. {
  283. var maxTries=5;
  284. var apiURL='https://filmot.com/api/getvideos?key=md5paNgdbaeudounjp39&id='+ window.deletedIDs;
  285. var jqxhr = $.getJSON(apiURL, function(data) {
  286. if (format==1) {
  287. processJSONResultSideView(data,format);
  288. }
  289. else
  290. {
  291. processJSONResultFullView(data,format);
  292. }
  293. })
  294. .done(function(data) {
  295. })
  296. .fail(function(error) {
  297. if (nTry>=maxTries) {
  298. reportAJAXError(apiURL + " " + JSON.stringify(error));
  299. return;
  300. }
  301. processClick(format,nTry+1);
  302. })
  303. .always(function() {
  304. });
  305.  
  306. }
  307.  
  308. function ButtonClickActionSideView (zEvent) {
  309. processClick(1,0);
  310. return false;
  311. }
  312.  
  313. function ButtonClickActionFullView (zEvent) {
  314. processClick(2,0);
  315. return false;
  316. }
  317.