Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

newsfeed.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. /* Magic Mirror
  2. * Module: NewsFeed
  3. *
  4. * By Michael Teeuw https://michaelteeuw.nl
  5. * MIT Licensed.
  6. */
  7. Module.register("newsfeed", {
  8. // Default module config.
  9. defaults: {
  10. feeds: [
  11. {
  12. title: "New York Times",
  13. url: "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
  14. encoding: "UTF-8" //ISO-8859-1
  15. }
  16. ],
  17. showAsList: false,
  18. showSourceTitle: true,
  19. showPublishDate: true,
  20. broadcastNewsFeeds: true,
  21. broadcastNewsUpdates: true,
  22. showDescription: false,
  23. wrapTitle: true,
  24. wrapDescription: true,
  25. truncDescription: true,
  26. lengthDescription: 400,
  27. hideLoading: false,
  28. reloadInterval: 5 * 60 * 1000, // every 5 minutes
  29. updateInterval: 10 * 1000,
  30. animationSpeed: 2.5 * 1000,
  31. maxNewsItems: 0, // 0 for unlimited
  32. ignoreOldItems: false,
  33. ignoreOlderThan: 24 * 60 * 60 * 1000, // 1 day
  34. removeStartTags: "",
  35. removeEndTags: "",
  36. startTags: [],
  37. endTags: [],
  38. prohibitedWords: [],
  39. scrollLength: 500,
  40. logFeedWarnings: false
  41. },
  42. // Define required scripts.
  43. getScripts: function () {
  44. return ["moment.js"];
  45. },
  46. //Define required styles.
  47. getStyles: function () {
  48. return ["newsfeed.css"];
  49. },
  50. // Define required translations.
  51. getTranslations: function () {
  52. // The translations for the default modules are defined in the core translation files.
  53. // Therefor we can just return false. Otherwise we should have returned a dictionary.
  54. // If you're trying to build your own module including translations, check out the documentation.
  55. return false;
  56. },
  57. // Define start sequence.
  58. start: function () {
  59. Log.info("Starting module: " + this.name);
  60. // Set locale.
  61. moment.locale(config.language);
  62. this.newsItems = [];
  63. this.loaded = false;
  64. this.error = null;
  65. this.activeItem = 0;
  66. this.scrollPosition = 0;
  67. this.registerFeeds();
  68. this.isShowingDescription = this.config.showDescription;
  69. },
  70. // Override socket notification handler.
  71. socketNotificationReceived: function (notification, payload) {
  72. if (notification === "NEWS_ITEMS") {
  73. this.generateFeed(payload);
  74. if (!this.loaded) {
  75. if (this.config.hideLoading) {
  76. this.show();
  77. }
  78. this.scheduleUpdateInterval();
  79. }
  80. this.loaded = true;
  81. this.error = null;
  82. } else if (notification === "NEWSFEED_ERROR") {
  83. this.error = this.translate(payload.error_type);
  84. this.scheduleUpdateInterval();
  85. }
  86. },
  87. //Override fetching of template name
  88. getTemplate: function () {
  89. if (this.config.feedUrl) {
  90. return "oldconfig.njk";
  91. } else if (this.config.showFullArticle) {
  92. return "fullarticle.njk";
  93. }
  94. return "newsfeed.njk";
  95. },
  96. //Override template data and return whats used for the current template
  97. getTemplateData: function () {
  98. // this.config.showFullArticle is a run-time configuration, triggered by optional notifications
  99. if (this.config.showFullArticle) {
  100. return {
  101. url: this.getActiveItemURL()
  102. };
  103. }
  104. if (this.error) {
  105. return {
  106. error: this.error
  107. };
  108. }
  109. if (this.newsItems.length === 0) {
  110. return {
  111. loaded: false
  112. };
  113. }
  114. if (this.activeItem >= this.newsItems.length) {
  115. this.activeItem = 0;
  116. }
  117. const item = this.newsItems[this.activeItem];
  118. const items = this.newsItems.map(function (item) {
  119. item.publishDate = moment(new Date(item.pubdate)).fromNow();
  120. return item;
  121. });
  122. return {
  123. loaded: true,
  124. config: this.config,
  125. sourceTitle: item.sourceTitle,
  126. publishDate: moment(new Date(item.pubdate)).fromNow(),
  127. title: item.title,
  128. description: item.description,
  129. items: items
  130. };
  131. },
  132. getActiveItemURL: function () {
  133. return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
  134. },
  135. /**
  136. * Registers the feeds to be used by the backend.
  137. */
  138. registerFeeds: function () {
  139. for (let feed of this.config.feeds) {
  140. this.sendSocketNotification("ADD_FEED", {
  141. feed: feed,
  142. config: this.config
  143. });
  144. }
  145. },
  146. /**
  147. * Generate an ordered list of items for this configured module.
  148. *
  149. * @param {object} feeds An object with feeds returned by the node helper.
  150. */
  151. generateFeed: function (feeds) {
  152. let newsItems = [];
  153. for (let feed in feeds) {
  154. const feedItems = feeds[feed];
  155. if (this.subscribedToFeed(feed)) {
  156. for (let item of feedItems) {
  157. item.sourceTitle = this.titleForFeed(feed);
  158. if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
  159. newsItems.push(item);
  160. }
  161. }
  162. }
  163. }
  164. newsItems.sort(function (a, b) {
  165. const dateA = new Date(a.pubdate);
  166. const dateB = new Date(b.pubdate);
  167. return dateB - dateA;
  168. });
  169. if (this.config.maxNewsItems > 0) {
  170. newsItems = newsItems.slice(0, this.config.maxNewsItems);
  171. }
  172. if (this.config.prohibitedWords.length > 0) {
  173. newsItems = newsItems.filter(function (item) {
  174. for (let word of this.config.prohibitedWords) {
  175. if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
  176. return false;
  177. }
  178. }
  179. return true;
  180. }, this);
  181. }
  182. newsItems.forEach((item) => {
  183. //Remove selected tags from the beginning of rss feed items (title or description)
  184. if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
  185. for (let startTag of this.config.startTags) {
  186. if (item.title.slice(0, startTag.length) === startTag) {
  187. item.title = item.title.slice(startTag.length, item.title.length);
  188. }
  189. }
  190. }
  191. if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
  192. if (this.isShowingDescription) {
  193. for (let startTag of this.config.startTags) {
  194. if (item.description.slice(0, startTag.length) === startTag) {
  195. item.description = item.description.slice(startTag.length, item.description.length);
  196. }
  197. }
  198. }
  199. }
  200. //Remove selected tags from the end of rss feed items (title or description)
  201. if (this.config.removeEndTags) {
  202. for (let endTag of this.config.endTags) {
  203. if (item.title.slice(-endTag.length) === endTag) {
  204. item.title = item.title.slice(0, -endTag.length);
  205. }
  206. }
  207. if (this.isShowingDescription) {
  208. for (let endTag of this.config.endTags) {
  209. if (item.description.slice(-endTag.length) === endTag) {
  210. item.description = item.description.slice(0, -endTag.length);
  211. }
  212. }
  213. }
  214. }
  215. });
  216. // get updated news items and broadcast them
  217. const updatedItems = [];
  218. newsItems.forEach((value) => {
  219. if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
  220. // Add item to updated items list
  221. updatedItems.push(value);
  222. }
  223. });
  224. // check if updated items exist, if so and if we should broadcast these updates, then lets do so
  225. if (this.config.broadcastNewsUpdates && updatedItems.length > 0) {
  226. this.sendNotification("NEWS_FEED_UPDATE", { items: updatedItems });
  227. }
  228. this.newsItems = newsItems;
  229. },
  230. /**
  231. * Check if this module is configured to show this feed.
  232. *
  233. * @param {string} feedUrl Url of the feed to check.
  234. * @returns {boolean} True if it is subscribed, false otherwise
  235. */
  236. subscribedToFeed: function (feedUrl) {
  237. for (let feed of this.config.feeds) {
  238. if (feed.url === feedUrl) {
  239. return true;
  240. }
  241. }
  242. return false;
  243. },
  244. /**
  245. * Returns title for the specific feed url.
  246. *
  247. * @param {string} feedUrl Url of the feed
  248. * @returns {string} The title of the feed
  249. */
  250. titleForFeed: function (feedUrl) {
  251. for (let feed of this.config.feeds) {
  252. if (feed.url === feedUrl) {
  253. return feed.title || "";
  254. }
  255. }
  256. return "";
  257. },
  258. /**
  259. * Schedule visual update.
  260. */
  261. scheduleUpdateInterval: function () {
  262. this.updateDom(this.config.animationSpeed);
  263. // Broadcast NewsFeed if needed
  264. if (this.config.broadcastNewsFeeds) {
  265. this.sendNotification("NEWS_FEED", { items: this.newsItems });
  266. }
  267. this.timer = setInterval(() => {
  268. this.activeItem++;
  269. this.updateDom(this.config.animationSpeed);
  270. // Broadcast NewsFeed if needed
  271. if (this.config.broadcastNewsFeeds) {
  272. this.sendNotification("NEWS_FEED", { items: this.newsItems });
  273. }
  274. }, this.config.updateInterval);
  275. },
  276. resetDescrOrFullArticleAndTimer: function () {
  277. this.isShowingDescription = this.config.showDescription;
  278. this.config.showFullArticle = false;
  279. this.scrollPosition = 0;
  280. // reset bottom bar alignment
  281. document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
  282. if (!this.timer) {
  283. this.scheduleUpdateInterval();
  284. }
  285. },
  286. notificationReceived: function (notification, payload, sender) {
  287. const before = this.activeItem;
  288. if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
  289. this.hide();
  290. } else if (notification === "ARTICLE_NEXT") {
  291. this.activeItem++;
  292. if (this.activeItem >= this.newsItems.length) {
  293. this.activeItem = 0;
  294. }
  295. this.resetDescrOrFullArticleAndTimer();
  296. Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
  297. this.updateDom(100);
  298. } else if (notification === "ARTICLE_PREVIOUS") {
  299. this.activeItem--;
  300. if (this.activeItem < 0) {
  301. this.activeItem = this.newsItems.length - 1;
  302. }
  303. this.resetDescrOrFullArticleAndTimer();
  304. Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
  305. this.updateDom(100);
  306. }
  307. // if "more details" is received the first time: show article summary, on second time show full article
  308. else if (notification === "ARTICLE_MORE_DETAILS") {
  309. // full article is already showing, so scrolling down
  310. if (this.config.showFullArticle === true) {
  311. this.scrollPosition += this.config.scrollLength;
  312. window.scrollTo(0, this.scrollPosition);
  313. Log.debug(this.name + " - scrolling down");
  314. Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
  315. } else {
  316. this.showFullArticle();
  317. }
  318. } else if (notification === "ARTICLE_SCROLL_UP") {
  319. if (this.config.showFullArticle === true) {
  320. this.scrollPosition -= this.config.scrollLength;
  321. window.scrollTo(0, this.scrollPosition);
  322. Log.debug(this.name + " - scrolling up");
  323. Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
  324. }
  325. } else if (notification === "ARTICLE_LESS_DETAILS") {
  326. this.resetDescrOrFullArticleAndTimer();
  327. Log.debug(this.name + " - showing only article titles again");
  328. this.updateDom(100);
  329. } else if (notification === "ARTICLE_TOGGLE_FULL") {
  330. if (this.config.showFullArticle) {
  331. this.activeItem++;
  332. this.resetDescrOrFullArticleAndTimer();
  333. } else {
  334. this.showFullArticle();
  335. }
  336. } else if (notification === "ARTICLE_INFO_REQUEST") {
  337. this.sendNotification("ARTICLE_INFO_RESPONSE", {
  338. title: this.newsItems[this.activeItem].title,
  339. source: this.newsItems[this.activeItem].sourceTitle,
  340. date: this.newsItems[this.activeItem].pubdate,
  341. desc: this.newsItems[this.activeItem].description,
  342. url: this.getActiveItemURL()
  343. });
  344. }
  345. },
  346. showFullArticle: function () {
  347. this.isShowingDescription = !this.isShowingDescription;
  348. this.config.showFullArticle = !this.isShowingDescription;
  349. // make bottom bar align to top to allow scrolling
  350. if (this.config.showFullArticle === true) {
  351. document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
  352. }
  353. clearInterval(this.timer);
  354. this.timer = null;
  355. Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
  356. this.updateDom(100);
  357. }
  358. });