YouTube RSS Feed (hacky fork)

Adds an RSS feed button to YouTube channels next to the subscribe button

  1. // ==UserScript==
  2. // @name YouTube RSS Feed (hacky fork)
  3. // @namespace https://greasyfork.org/en/users/4612-gdorn
  4. // @author Doodles (original) with hacky fixes by George Dorn
  5. // @version 21
  6. // @description Adds an RSS feed button to YouTube channels next to the subscribe button
  7. // @icon http://i.imgur.com/Ty5HNbT.png
  8. // @icon64 http://i.imgur.com/1FfVvNr.png
  9. // @match *://www.youtube.com/*
  10. // @match *://youtube.com/*
  11. // @grant none
  12. // @run-at document-end
  13. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. this.$ = this.jQuery = jQuery.noConflict(true);
  18.  
  19. window.safejQuery = this.$;
  20.  
  21. window.getRSS = function () {
  22.  
  23. "use strict";
  24. addRssFeedSupport(true);
  25. document.body.addEventListener("yt-navigate-finish", function (event) {
  26. addRssFeedSupport(false);
  27. });
  28.  
  29. }
  30.  
  31. function addRssFeedSupport(firstLoad) {
  32. if (isPlaylistPage()) {
  33. waitForElement("owner-container", function () {
  34. let playlistFeedLink = getPlaylistFeed(getPlatlistId());
  35. addRssLink(playlistFeedLink);
  36. addRssButtonPlaylist(playlistFeedLink);
  37. }, 330);
  38. } else if (isVideoPage()) {
  39. waitForElement("upload-info", function () {
  40. let channelFeedLink = getChannelFeed(getChannelIdFromPage());
  41. removeRssLink();
  42. addRssLink(channelFeedLink);
  43. addRssButton(channelFeedLink);
  44. }, 330);
  45. } else if (isChannelPage()) {
  46. waitForElement("subscribe-button", function () {
  47. let channelId = getChannelIdFromPage();
  48. if (channelId === null && firstLoad) {
  49. removeRssLink();
  50. addRefreshButton();
  51. } else {
  52. let channelFeedLink = getChannelFeed(channelId);
  53. removeRssLink();
  54. addRssLink(channelFeedLink);
  55. addRssButton(channelFeedLink);
  56. }
  57. }, 330);
  58. }
  59. }
  60.  
  61. function isPlaylistPage() {
  62. return document.URL.indexOf("/playlist?list=") !== -1;
  63. }
  64.  
  65. function isVideoPage() {
  66. return document.URL.indexOf("/watch") !== -1 && document.URL.indexOf("v=") !== -1;
  67. }
  68.  
  69. function isChannelPage() {
  70. return $("#channel-header").length > 0;
  71. }
  72.  
  73. function getPlatlistId() {
  74. let playlistId = document.URL.split("list=")[1].split("&")[0];
  75. if (!playlistId.startsWith("PL")) {
  76. playlistId = "PL" + playlistId;
  77. }
  78. return playlistId;
  79. }
  80.  
  81. function getChannelIdFromPage() {
  82.  
  83. let channelId = null;
  84.  
  85. channelId = ytInitialPlayerResponse['videoDetails']['channelId'];
  86. if (channelId) {
  87. return channelId;
  88. }
  89.  
  90. // try URL
  91. channelId = getChannelIdFromUrl(document.URL);
  92. if (channelId) {
  93. return channelId;
  94. }
  95.  
  96. // try meta tags that are channel URLs
  97. let metaChannelUrlTags = [
  98. 'og:url',
  99. 'al:ios:url',
  100. 'al:android:url',
  101. 'al:web:url',
  102. 'twitter:url',
  103. 'twitter:app:url:iphone',
  104. 'twitter:app:url:ipad'
  105. ];
  106. for (let i = 0; i < metaChannelUrlTags.length; i++) {
  107. let metaPropertyValue = getMetaTagValue(metaChannelUrlTags[i]);
  108. channelId = metaPropertyValue ? getChannelIdFromUrl(metaPropertyValue) : null;
  109. if (channelId) {
  110. return channelId;
  111. }
  112. }
  113.  
  114. // try meta tags that are channel IDs
  115. let metaChannelIdTags = [
  116. 'channelId'
  117. ];
  118. for (let i = 0; i < metaChannelIdTags.length; i++) {
  119. channelId = getMetaTagValue(metaChannelIdTags[i]);
  120. if (channelId) {
  121. return channelId;
  122. }
  123. }
  124.  
  125. // try upload info box on video page
  126. let uploadInfoLink = $("#upload-info a[href*='/channel/']:first");
  127. if (uploadInfoLink.length) {
  128. let uploadInfoLinkHref = uploadInfoLink.attr("href");
  129. channelId = uploadInfoLinkHref ? getChannelIdFromUrl(uploadInfoLinkHref) : null;
  130. if (channelId) {
  131. return channelId;
  132. }
  133. }
  134.  
  135. // give up
  136. return null;
  137. }
  138.  
  139. function getChannelIdFromUrl(url) {
  140. if (url && url.indexOf("youtube.com/channel/") !== -1) {
  141. return url.split("youtube.com/channel/")[1].split("/")[0].split("?")[0];
  142. } else {
  143. return null;
  144. }
  145. }
  146.  
  147. function getMetaTagValue(metaPropertyKey) {
  148. // <meta property="og:url" content="https://www.youtube.com/channel/UCC8VtutLDSrreNEZj8CU01A">
  149. // <meta name="twitter:url" content="https://www.youtube.com/channel/UCC8VtutLDSrreNEZj8CU01A">
  150. // <meta itemprop="channelId" content="UCC8VtutLDSrreNEZj8CU01A">
  151.  
  152. console.log("Trying to get metatagvalue for", metaPropertyKey);
  153.  
  154. let nameAttributes = ['property', 'name', 'itemprop'];
  155. let metaProperty = null;
  156. for (let i = 0; i < nameAttributes.length; i++) {
  157. metaProperty = $("meta[" + nameAttributes[i] + "='" + metaPropertyKey + "']");
  158. console.log("Looking in:", metaProperty);
  159. if (metaProperty.length === 1) {
  160. break;
  161. }
  162. metaProperty = null;
  163. }
  164.  
  165. if (metaProperty !== null) {
  166. let value = metaProperty.attr("content");
  167. if (value) {
  168. return value;
  169. }
  170. }
  171. return null;
  172. }
  173.  
  174. function getChannelFeed(channelId) {
  175. return "https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId;
  176. }
  177.  
  178. function getPlaylistFeed(playlistId) {
  179. return "https://www.youtube.com/feeds/videos.xml?playlist_id=" + playlistId;
  180. }
  181.  
  182. function addRssLink(link) {
  183. $("head").append('<link rel="alternate" type="application/rss+xml" title="RSS" href="' +
  184. link + '" />');
  185. }
  186.  
  187. function removeRssLink() {
  188. if ($("link[type='application/rss+xml']").length > 0) {
  189. $("link[type='application/rss+xml']").remove();
  190. }
  191. }
  192.  
  193. function waitForElement(elementId, callbackFunction, intervalLength = 330) {
  194. var waitCount = 15000 / intervalLength; // wait 15 seconds maximum
  195. var wait = setInterval(function () {
  196. waitCount--;
  197. if ($("#" + elementId).length > 0) {
  198. callbackFunction();
  199. clearInterval(wait);
  200. } else if (waitCount <= 0) {
  201. console.log("YouTube RSS Feed UserScript - wait for element \"#" + elementId +
  202. "\" failed! Time limit (15 seconds) exceeded.");
  203. clearInterval(wait);
  204. }
  205. }, intervalLength);
  206. }
  207.  
  208. function addRssButton(link) {
  209. if ($("#rssSubButton").length > 0) {
  210. $("#rssSubButton").remove();
  211. }
  212. $("#subscribe-button")
  213. .css({
  214. "display": "flex",
  215. "flex-flow": "nowrap",
  216. "height": "37px"
  217. })
  218. .prepend(makeRssButton(link));
  219. }
  220.  
  221. function addRssButtonPlaylist(link) {
  222. if ($("#rssSubButton").length === 0) {
  223. $("#owner-container > #button")
  224. .css({
  225. "display": "flex",
  226. "flex-flow": "nowrap",
  227. "height": "37px"
  228. })
  229. .prepend(makeRssButton(link));
  230. }
  231. }
  232.  
  233. function makeRssButton(link) {
  234. return $("<a>RSS</a>")
  235. .attr("id", "rssSubButton")
  236. .attr("target", "_blank")
  237. .attr("href", rssLinkToData(link))
  238. .attr("download", "feed.rss")
  239. .css({
  240. "background-color": "#fd9b12",
  241. "border-radius": "3px",
  242. "padding": "10px 16px",
  243. "color": "#ffffff",
  244. "font-size": "14px",
  245. "text-decoration": "none",
  246. "text-transform": "uppercase",
  247. "margin-right": "5px"
  248. });
  249. }
  250.  
  251. function rssLinkToData(link) {
  252. return link;
  253. return "data:application/atom+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
  254. "<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
  255. "<title type=\"text\">YouTube RSS Button</title>" +
  256. "<link rel=\"self\" href=\"" + link + "\" type=\"application/atom+xml\" />" +
  257. "</feed>";
  258. }
  259.  
  260. function addRefreshButton() {
  261. let refreshButton = $("<a>Refresh</a>")
  262. .attr("id", "rssSubButton")
  263. .attr("href", "#")
  264. .css({
  265. "background-color": "#fd9b12",
  266. "border-radius": "3px",
  267. "padding": "10px 16px",
  268. "color": "#ffffff",
  269. "font-size": "14px",
  270. "text-decoration": "none",
  271. "text-transform": "uppercase",
  272. "margin-right": "5px"
  273. });
  274. $(refreshButton).click(function (e) {
  275. e.preventDefault();
  276. let r = confirm("Due to how YouTube load pages, there isn't a reliable way to get channel" +
  277. " IDs from channel pages if you've navigated to them from another YouTube page." +
  278. " The solution is to reload the page.\n\nWould you like to reload the page?");
  279. if (r === true) {
  280. window.location.reload();
  281. }
  282. });
  283. if ($("#rssSubButton").length > 0) {
  284. $("#rssSubButton").remove();
  285. }
  286. $("#subscribe-button")
  287. .css({
  288. "display": "flex",
  289. "flex-flow": "nowrap",
  290. "height": "37px"
  291. })
  292. .prepend(refreshButton);
  293. }
  294.  
  295. $(window.getRSS());