Greasy Fork is available in English.

YouTube RSS Feed

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

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