@@ -4,41 +4,71 @@ const FeedMe = require("feedme");
44const iconv = require ( "iconv-lite" ) ;
55const { htmlToText } = require ( "html-to-text" ) ;
66const Log = require ( "logger" ) ;
7- const NodeHelper = require ( "node_helper" ) ;
8- const { getUserAgent } = require ( "#server_functions" ) ;
9- const { scheduleTimer } = require ( "#module_functions" ) ;
7+ const HTTPFetcher = require ( "#http_fetcher" ) ;
108
119/**
12- * Responsible for requesting an update on the set interval and broadcasting the data.
13- * @param {string } url URL of the news feed.
14- * @param {number } reloadInterval Reload interval in milliseconds.
15- * @param {string } encoding Encoding of the feed.
16- * @param {boolean } logFeedWarnings If true log warnings when there is an error parsing a news article.
17- * @param {boolean } useCorsProxy If true cors proxy is used for article url's.
10+ * NewsfeedFetcher - Fetches and parses RSS/Atom feed data
11+ * Uses HTTPFetcher for HTTP handling with intelligent error handling
1812 * @class
1913 */
20- const NewsfeedFetcher = function ( url , reloadInterval , encoding , logFeedWarnings , useCorsProxy ) {
21- let reloadTimer = null ;
22- let items = [ ] ;
23- let reloadIntervalMS = reloadInterval ;
14+ class NewsfeedFetcher {
2415
25- let fetchFailedCallback = function ( ) { } ;
26- let itemsReceivedCallback = function ( ) { } ;
16+ /**
17+ * Creates a new NewsfeedFetcher instance
18+ * @param {string } url - The URL of the news feed to fetch
19+ * @param {number } reloadInterval - Time in ms between fetches
20+ * @param {string } encoding - Encoding of the feed (e.g., 'UTF-8')
21+ * @param {boolean } logFeedWarnings - If true log warnings when there is an error parsing a news article
22+ * @param {boolean } useCorsProxy - If true cors proxy is used for article url's
23+ */
24+ constructor ( url , reloadInterval , encoding , logFeedWarnings , useCorsProxy ) {
25+ this . url = url ;
26+ this . encoding = encoding ;
27+ this . logFeedWarnings = logFeedWarnings ;
28+ this . useCorsProxy = useCorsProxy ;
29+ this . items = [ ] ;
30+ this . fetchFailedCallback = ( ) => { } ;
31+ this . itemsReceivedCallback = ( ) => { } ;
32+
33+ // Use HTTPFetcher for HTTP handling (Composition)
34+ this . httpFetcher = new HTTPFetcher ( url , {
35+ reloadInterval : Math . max ( reloadInterval , 1000 ) ,
36+ headers : {
37+ "Cache-Control" : "max-age=0, no-cache, no-store, must-revalidate" ,
38+ Pragma : "no-cache"
39+ }
40+ } ) ;
2741
28- if ( reloadIntervalMS < 1000 ) {
29- reloadIntervalMS = 1000 ;
42+ // Wire up HTTPFetcher events
43+ this . httpFetcher . on ( "response" , ( response ) => this . #handleResponse( response ) ) ;
44+ this . httpFetcher . on ( "error" , ( errorInfo ) => this . fetchFailedCallback ( this , errorInfo ) ) ;
3045 }
3146
32- /* private methods */
33-
3447 /**
35- * Request the new items.
48+ * Creates a parse error info object
49+ * @param {string } message - Error message
50+ * @param {Error } error - Original error
51+ * @returns {object } Error info object
3652 */
37- const fetchNews = ( ) => {
38- clearTimeout ( reloadTimer ) ;
39- reloadTimer = null ;
40- items = [ ] ;
53+ #createParseError ( message , error ) {
54+ return {
55+ message,
56+ status : null ,
57+ errorType : "PARSE_ERROR" ,
58+ translationKey : "MODULE_ERROR_UNSPECIFIED" ,
59+ retryAfter : this . httpFetcher . reloadInterval ,
60+ retryCount : 0 ,
61+ url : this . url ,
62+ originalError : error
63+ } ;
64+ }
4165
66+ /**
67+ * Handles successful HTTP response
68+ * @param {Response } response - The fetch Response object
69+ */
70+ #handleResponse ( response ) {
71+ this . items = [ ] ;
4272 const parser = new FeedMe ( ) ;
4373
4474 parser . on ( "item" , ( item ) => {
@@ -58,118 +88,80 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
5888 ]
5989 } ) ;
6090
61- items . push ( {
62- title : title ,
63- description : description ,
64- pubdate : pubdate ,
65- url : url ,
66- useCorsProxy : useCorsProxy ,
91+ this . items . push ( {
92+ title,
93+ description,
94+ pubdate,
95+ url,
96+ useCorsProxy : this . useCorsProxy ,
6797 hash : crypto . createHash ( "sha256" ) . update ( `${ pubdate } :: ${ title } :: ${ url } ` ) . digest ( "hex" )
6898 } ) ;
69- } else if ( logFeedWarnings ) {
99+ } else if ( this . logFeedWarnings ) {
70100 Log . warn ( "Can't parse feed item:" , item ) ;
71101 Log . warn ( `Title: ${ title } ` ) ;
72102 Log . warn ( `Description: ${ description } ` ) ;
73103 Log . warn ( `Pubdate: ${ pubdate } ` ) ;
74104 }
75105 } ) ;
76106
77- parser . on ( "end" , ( ) => {
78- this . broadcastItems ( ) ;
79- } ) ;
107+ parser . on ( "end" , ( ) => this . broadcastItems ( ) ) ;
80108
81109 parser . on ( "error" , ( error ) => {
82- fetchFailedCallback ( this , error ) ;
83- scheduleTimer ( reloadTimer , reloadIntervalMS , fetchNews ) ;
84- } ) ;
85-
86- //"end" event is not broadcast if the feed is empty but "finish" is used for both
87- parser . on ( "finish" , ( ) => {
88- scheduleTimer ( reloadTimer , reloadIntervalMS , fetchNews ) ;
110+ Log . error ( `${ this . url } - Feed parsing failed: ${ error . message } ` ) ;
111+ this . fetchFailedCallback ( this , this . #createParseError( `Feed parsing failed: ${ error . message } ` , error ) ) ;
89112 } ) ;
90113
91114 parser . on ( "ttl" , ( minutes ) => {
92- try {
93- // 86400000 = 24 hours is mentioned in the docs as maximum value:
94- const ttlms = Math . min ( minutes * 60 * 1000 , 86400000 ) ;
95- if ( ttlms > reloadIntervalMS ) {
96- reloadIntervalMS = ttlms ;
97- Log . info ( `reloadInterval set to ttl=${ reloadIntervalMS } for url ${ url } ` ) ;
98- }
99- } catch ( error ) {
100- Log . warn ( `feed ttl is no valid integer=${ minutes } for url ${ url } ` ) ;
115+ const ttlms = Math . min ( minutes * 60 * 1000 , 86400000 ) ;
116+ if ( ttlms > this . httpFetcher . reloadInterval ) {
117+ this . httpFetcher . reloadInterval = ttlms ;
118+ Log . info ( `reloadInterval set to ttl=${ ttlms } for url ${ this . url } ` ) ;
101119 }
102120 } ) ;
103121
104- const headers = {
105- "User-Agent" : getUserAgent ( ) ,
106- "Cache-Control" : "max-age=0, no-cache, no-store, must-revalidate" ,
107- Pragma : "no-cache"
108- } ;
109-
110- fetch ( url , { headers : headers } )
111- . then ( NodeHelper . checkFetchStatus )
112- . then ( ( response ) => {
113- let nodeStream ;
114- if ( response . body instanceof stream . Readable ) {
115- nodeStream = response . body ;
116- } else {
117- nodeStream = stream . Readable . fromWeb ( response . body ) ;
118- }
119- nodeStream . pipe ( iconv . decodeStream ( encoding ) ) . pipe ( parser ) ;
120- } )
121- . catch ( ( error ) => {
122- fetchFailedCallback ( this , error ) ;
123- scheduleTimer ( reloadTimer , reloadIntervalMS , fetchNews ) ;
124- } ) ;
125- } ;
126-
127- /* public methods */
122+ try {
123+ const nodeStream = response . body instanceof stream . Readable
124+ ? response . body
125+ : stream . Readable . fromWeb ( response . body ) ;
126+ nodeStream . pipe ( iconv . decodeStream ( this . encoding ) ) . pipe ( parser ) ;
127+ } catch ( error ) {
128+ Log . error ( `${ this . url } - Stream processing failed: ${ error . message } ` ) ;
129+ this . fetchFailedCallback ( this , this . #createParseError( `Stream processing failed: ${ error . message } ` , error ) ) ;
130+ }
131+ }
128132
129133 /**
130134 * Update the reload interval, but only if we need to increase the speed.
131- * @param {number } interval Interval for the update in milliseconds.
135+ * @param {number } interval - Interval for the update in milliseconds.
132136 */
133- this . setReloadInterval = function ( interval ) {
134- if ( interval > 1000 && interval < reloadIntervalMS ) {
135- reloadIntervalMS = interval ;
137+ setReloadInterval ( interval ) {
138+ if ( interval > 1000 && interval < this . httpFetcher . reloadInterval ) {
139+ this . httpFetcher . reloadInterval = interval ;
136140 }
137- } ;
141+ }
138142
139- /**
140- * Initiate fetchNews();
141- */
142- this . startFetch = function ( ) {
143- fetchNews ( ) ;
144- } ;
143+ startFetch ( ) {
144+ this . httpFetcher . startPeriodicFetch ( ) ;
145+ }
145146
146- /**
147- * Broadcast the existing items.
148- */
149- this . broadcastItems = function ( ) {
150- if ( items . length <= 0 ) {
147+ broadcastItems ( ) {
148+ if ( this . items . length <= 0 ) {
151149 Log . info ( "No items to broadcast yet." ) ;
152150 return ;
153151 }
154- Log . info ( `Broadcasting ${ items . length } items.` ) ;
155- itemsReceivedCallback ( this ) ;
156- } ;
157-
158- this . onReceive = function ( callback ) {
159- itemsReceivedCallback = callback ;
160- } ;
161-
162- this . onError = function ( callback ) {
163- fetchFailedCallback = callback ;
164- } ;
165-
166- this . url = function ( ) {
167- return url ;
168- } ;
169-
170- this . items = function ( ) {
171- return items ;
172- } ;
173- } ;
152+ Log . info ( `Broadcasting ${ this . items . length } items.` ) ;
153+ this . itemsReceivedCallback ( this ) ;
154+ }
155+
156+ /** @param {function(NewsfeedFetcher): void } callback - Called when items are received */
157+ onReceive ( callback ) {
158+ this . itemsReceivedCallback = callback ;
159+ }
160+
161+ /** @param {function(NewsfeedFetcher, object): void } callback - Called on fetch error */
162+ onError ( callback ) {
163+ this . fetchFailedCallback = callback ;
164+ }
165+ }
174166
175167module . exports = NewsfeedFetcher ;
0 commit comments