Class: SimpleRSS

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/simple-rss.rb,
lib/simple-rss/request_errors.rb

Overview

rbs_inline: enabled

Defined Under Namespace

Classes: Discovery, DiscoveryDependencyError, DiscoveryError, EntryNormalizer, HTTPClient, HTTPError, JsonEntryNormalizer, JsonFeed, NormalizedEntry, PolicyError, RedirectError, RequestError, RequestPolicy, RequestTimeout, ResponseTooLarge, XmlElement

Constant Summary collapse

VERSION =

@rbs! include Enumerable[Hash[Symbol, untyped]]

"2.3.0".freeze
DATE_TAGS =
%i[pubDate lastBuildDate published updated expirationDate modified dc:date].freeze
STRIP_HTML_TAGS =
%i[author contributor skipHours skipDays].freeze
ATOM_NAMESPACE =
"http://www.w3.org/2005/Atom".freeze
RSS_NAMESPACES =
[nil, "", "http://purl.org/rss/1.0/", "http://my.netscape.com/rdf/simple/0.9/"].freeze
XML_TAG_PATTERN =
%r{<!--.*?-->|<!\[CDATA\[.*?\]\]>|<\?.*?\?>|<(/?)([\w:.-]+)((?:[^<>"']|"[^"]*"|'[^']*')*)>}m
@@feed_tags =
%i[
  id
  title subtitle link
  description
  author webMaster managingEditor contributor
  pubDate lastBuildDate updated dc:date
  generator language docs cloud
  ttl skipHours skipDays
  image logo icon rating
  rights copyright
  textInput feedburner:browserFriendly
  itunes:author itunes:category
]
@@item_tags =
%i[
  id
  title link link+alternate link+self link+edit link+replies
  author contributor
  description summary content content:encoded comments
  pubDate published updated expirationDate modified dc:date
  category guid
  trackback:ping trackback:about
  dc:creator dc:title dc:subject dc:rights dc:publisher
  feedburner:origLink
  media:content#url media:content#type media:content#height media:content#width media:content#duration
  media:title media:thumbnail#url media:thumbnail#height media:thumbnail#width
  media:credit media:credit#role
  media:category media:category#scheme
  media:description
  enclosure#url enclosure#type enclosure#length
  itunes:duration itunes:image#href
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, options = {}) ⇒ SimpleRSS

Returns a new instance of SimpleRSS.



68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/simple-rss.rb', line 68

def initialize(source, options = {})
  @source = source.respond_to?(:read) ? source.read.to_s : source.to_s
  @items = [] #: Array[Hash[Symbol, untyped]]
  @options = {} #: Hash[Symbol, untyped]
  @options.update(options)
  @source_url = options[:source_url]
  @json_feed = nil
  @raw_json = nil
  @entry_contexts = {} #: Hash[Hash[Symbol, untyped], Hash[Symbol, untyped]]
  @entry_contexts.compare_by_identity

  parse
end

Instance Attribute Details

#authorsObject (readonly)

: untyped



30
31
32
# File 'lib/simple-rss.rb', line 30

def authors
  @authors
end

#etagObject (readonly)

: String?



25
26
27
# File 'lib/simple-rss.rb', line 25

def etag
  @etag
end

#expiredObject (readonly)

: bool?



31
32
33
# File 'lib/simple-rss.rb', line 31

def expired
  @expired
end

#faviconObject (readonly)

: String?



29
30
31
# File 'lib/simple-rss.rb', line 29

def favicon
  @favicon
end

#feed_urlObject (readonly)

: String?



29
30
31
# File 'lib/simple-rss.rb', line 29

def feed_url
  @feed_url
end

#home_page_urlObject (readonly)

: String?



29
30
31
# File 'lib/simple-rss.rb', line 29

def home_page_url
  @home_page_url
end

#hubsObject (readonly)

: untyped



30
31
32
# File 'lib/simple-rss.rb', line 30

def hubs
  @hubs
end

#itemsObject (readonly) Also known as: entries

Returns the value of attribute items.



23
24
25
# File 'lib/simple-rss.rb', line 23

def items
  @items
end

#last_modifiedObject (readonly)

: String?



26
27
28
# File 'lib/simple-rss.rb', line 26

def last_modified
  @last_modified
end

#next_urlObject (readonly)

: String?



29
30
31
# File 'lib/simple-rss.rb', line 29

def next_url
  @next_url
end

#raw_jsonObject (readonly)

: Hash[String, untyped]?



28
29
30
# File 'lib/simple-rss.rb', line 28

def raw_json
  @raw_json
end

#sourceObject (readonly)

: String



24
25
26
# File 'lib/simple-rss.rb', line 24

def source
  @source
end

#source_urlObject (readonly)

: String?



27
28
29
# File 'lib/simple-rss.rb', line 27

def source_url
  @source_url
end

#user_commentObject (readonly)

: String?



29
30
31
# File 'lib/simple-rss.rb', line 29

def user_comment
  @user_comment
end

Class Method Details

.discover(url, options = {}) ⇒ Object



332
333
334
335
# File 'lib/simple-rss.rb', line 332

def discover(url, options = {})
  require_relative "simple-rss/discovery"
  Discovery.new(options).discover(url)
end

.feed_tagsObject



267
268
269
# File 'lib/simple-rss.rb', line 267

def feed_tags
  @@feed_tags
end

.feed_tags=(ft) ⇒ Object



272
273
274
# File 'lib/simple-rss.rb', line 272

def feed_tags=(ft)
  @@feed_tags = ft
end

.fetch(url, options = {}) ⇒ Object

Fetch and parse a feed from a URL Returns nil if conditional GET returns 304 Not Modified

Raises:



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/simple-rss.rb', line 313

def fetch(url, options = {})
  require "net/http"
  require "uri"

  require_relative "simple-rss/http_client"
  response, final_uri = HTTPClient.new(options).get(url)

  return nil if response.is_a?(Net::HTTPNotModified)

  raise SimpleRSSError, "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)

  body = (response.body || "").force_encoding(Encoding::UTF_8)
  feed = parse(body, options.merge(source_url: final_uri.to_s))
  feed.instance_variable_set(:@etag, response["ETag"])
  feed.instance_variable_set(:@last_modified, response["Last-Modified"])
  feed
end

.item_tagsObject



277
278
279
# File 'lib/simple-rss.rb', line 277

def item_tags
  @@item_tags
end

.item_tags=(it) ⇒ Object



282
283
284
# File 'lib/simple-rss.rb', line 282

def item_tags=(it)
  @@item_tags = it
end

.merge(*feeds) ⇒ Object



302
303
304
305
306
307
# File 'lib/simple-rss.rb', line 302

def merge(*feeds)
  first_feed = feeds.first
  return [] if first_feed.nil?

  first_feed.merge(*feeds.drop(1))
end

.parse(source, options = {}) ⇒ Object

The strict attribute is for compatibility with Ruby's standard RSS parser



289
290
291
# File 'lib/simple-rss.rb', line 289

def parse(source, options = {})
  new source, options
end

.valid?(source, options = {}) ⇒ Boolean

Returns:

  • (Boolean)


294
295
296
297
298
299
# File 'lib/simple-rss.rb', line 294

def valid?(source, options = {})
  parse(source, options)
  true
rescue StandardError
  false
end

Instance Method Details

#[](index) ⇒ Object

Access an item by index



118
119
120
# File 'lib/simple-rss.rb', line 118

def [](index)
  items[index]
end

#as_json(_options = {}) ⇒ Object Also known as: to_hash



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/simple-rss.rb', line 229

def as_json(_options = {})
  raw_json = @raw_json
  hash = raw_json ? raw_json.transform_keys(&:to_sym) : {} #: Hash[Symbol, untyped]

  @@feed_tags.each do |tag|
    tag_cleaned = clean_tag(tag)
    value = instance_variable_get("@#{tag_cleaned}")
    hash[tag_cleaned] = serialize_value(value) if value
  end

  hash[:items] = items.map do |item|
    item.transform_values { |v| serialize_value(v) }
  end

  hash
end

#channelObject Also known as: feed



83
84
85
# File 'lib/simple-rss.rb', line 83

def channel
  self
end

#dedupeObject



203
204
205
206
# File 'lib/simple-rss.rb', line 203

def dedupe
  @items = dedupe_items(items)
  self
end

#diff(other) ⇒ Object



192
193
194
195
196
197
198
199
200
# File 'lib/simple-rss.rb', line 192

def diff(other)
  other_keys = keyed_item_set(other.items)
  current_keys = keyed_item_set(items)

  {
    added: select_new_keyed_items(other.items, current_keys),
    removed: select_new_keyed_items(items, other_keys)
  }
end

#each(&block) ⇒ Object

Iterate over all items in the feed



108
109
110
111
112
113
# File 'lib/simple-rss.rb', line 108

def each(&block)
  return enum_for(:each) unless block

  items.each(&block)
  self
end

#enclosuresObject



209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/simple-rss.rb', line 209

def enclosures
  items.filter_map do |item|
    enclosure_url = item[:enclosure_url]
    next if blank_value?(enclosure_url)

    {
      url: enclosure_url,
      type: item[:enclosure_type],
      length: item[:enclosure_length],
      item: item
    }
  end
end

#feed_typeObject



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/simple-rss.rb', line 130

def feed_type
  return :json_feed if @json_feed

  atom_namespaced_feed = source.match?(/<(atom:)?feed\b[^>]*xmlns(:\w+)?=['"][^'"]*atom/i)
  return :atom if atom_namespaced_feed
  return :rss2 if source.match?(/<rss[^>]*version=['"]2/i)
  return :rss1 if source.match?(/<rdf:RDF/i)
  return :rss09 if source.match?(/<rss[^>]*version=['"]0\.9/i)

  :unknown
end

#imagesObject



224
225
226
# File 'lib/simple-rss.rb', line 224

def images
  items.flat_map { |item| item_image_urls(item) }.uniq
end

#items_by_category(name) ⇒ Object



164
165
166
167
168
169
170
171
172
173
# File 'lib/simple-rss.rb', line 164

def items_by_category(name)
  query = name.to_s.downcase

  items.select do |item|
    category = item[:category]
    next false if category.nil?

    category_matches_query?(category, query)
  end
end

#items_since(time) ⇒ Object



156
157
158
159
160
161
# File 'lib/simple-rss.rb', line 156

def items_since(time)
  items.select do |item|
    date = item_date(item)
    date && date > time
  end
end

#latest(count = 10) ⇒ Object

Get the n most recent items, sorted by date



125
126
127
# File 'lib/simple-rss.rb', line 125

def latest(count = 10)
  sorted_items_by_date(items).first(count)
end

#merge(*feeds) ⇒ Object



185
186
187
188
189
# File 'lib/simple-rss.rb', line 185

def merge(*feeds)
  all_items = [items, *feeds.map(&:items)].flatten
  keyed_items, unkeyed_items = all_items.partition { |item| !item_key(item).nil? }
  dedupe_items(sorted_items_by_date(keyed_items) + unkeyed_items)
end

#normalized_entries(source_url: nil, mappings: {}) ⇒ Object

Raises:

  • (ArgumentError)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/simple-rss.rb', line 89

def normalized_entries(source_url: nil, mappings: {})
  json_feed = @json_feed
  raise ArgumentError, "XML mappings are not supported for JSON Feed" if json_feed && !mappings.empty?
  return items.map { |item| json_feed.normalized_entry(item, source_url: source_url || @source_url) } if json_feed

  EntryNormalizer.validate_mappings(mappings)
  feed_authors = normalized_feed_authors
  items.map do |item|
    context = @entry_contexts[item] || raise(SimpleRSSError, "Cannot normalize an item without its original XML source")
    element = XmlElement.new(context[:name], context[:attributes], context[:content], context[:parent])
    EntryNormalizer.new(element, item, source_url: source_url || @source_url, mappings: mappings,
                                       raw_xml: context[:xml], feed_authors: feed_authors).entry
  end
end

#search(query) ⇒ Object



176
177
178
179
180
181
182
# File 'lib/simple-rss.rb', line 176

def search(query)
  pattern = Regexp.new(Regexp.escape(query.to_s), Regexp::IGNORECASE)

  items.select do |item|
    searchable_fields(item).any? { |field| field.to_s.match?(pattern) }
  end
end

#to_jsonObject



247
248
249
250
# File 'lib/simple-rss.rb', line 247

def to_json(*)
  require "json"
  JSON.generate(as_json)
end

#to_xml(format: :rss2) ⇒ Object

Raises:



255
256
257
258
259
260
261
262
263
# File 'lib/simple-rss.rb', line 255

def to_xml(format: :rss2)
  raise SimpleRSSError, "JSON Feed to XML conversion is not supported" if @json_feed

  case format
  when :rss2 then to_rss2_xml
  when :atom then to_atom_xml
  else raise ArgumentError, "Unknown format: #{format}. Supported: :rss2, :atom"
  end
end

#valid?Boolean

Returns:

  • (Boolean)


143
144
145
146
147
148
149
150
151
152
153
# File 'lib/simple-rss.rb', line 143

def valid?
  return true if @json_feed

  return false if items.empty?

  title_value = instance_variable_get(:@title)
  link_value = instance_variable_get(:@link)
  return true if title_value || link_value

  false
end