feedtools 0.2.10 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1634 @@
1
+ module FeedTools
2
+ # The <tt>FeedTools::FeedItem</tt> class represents the structure of
3
+ # a single item within a web feed.
4
+ class FeedItem
5
+ include REXML
6
+
7
+ # This class stores information about a feed item's file enclosures.
8
+ class Enclosure
9
+ # The url for the enclosure
10
+ attr_accessor :url
11
+ # The MIME type of the file referenced by the enclosure
12
+ attr_accessor :type
13
+ # The size of the file referenced by the enclosure
14
+ attr_accessor :file_size
15
+ # The total play time of the file referenced by the enclosure
16
+ attr_accessor :duration
17
+ # The height in pixels of the enclosed media
18
+ attr_accessor :height
19
+ # The width in pixels of the enclosed media
20
+ attr_accessor :width
21
+ # The bitrate of the enclosed media
22
+ attr_accessor :bitrate
23
+ # The framerate of the enclosed media
24
+ attr_accessor :framerate
25
+ # The thumbnail for this enclosure
26
+ attr_accessor :thumbnail
27
+ # The categories for this enclosure
28
+ attr_accessor :categories
29
+ # A hash of the enclosed file
30
+ attr_accessor :hash
31
+ # A website containing some kind of media player instead of a direct
32
+ # link to the media file.
33
+ attr_accessor :player
34
+ # A list of credits for the enclosed media
35
+ attr_accessor :credits
36
+ # A text rendition of the enclosed media
37
+ attr_accessor :text
38
+ # A list of alternate version of the enclosed media file
39
+ attr_accessor :versions
40
+ # The default version of the enclosed media file
41
+ attr_accessor :default_version
42
+
43
+ # Returns true if this is the default enclosure
44
+ def is_default?
45
+ return @is_default
46
+ end
47
+
48
+ # Sets whether this is the default enclosure for the media group
49
+ def is_default=(new_is_default)
50
+ @is_default = new_is_default
51
+ end
52
+
53
+ # Returns true if the enclosure contains explicit material
54
+ def explicit?
55
+ return @explicit
56
+ end
57
+
58
+ # Sets the explicit attribute on the enclosure
59
+ def explicit=(new_explicit)
60
+ @explicit = new_explicit
61
+ end
62
+
63
+ # Determines if the object is a sample, or the full version of the
64
+ # object, or if it is a stream.
65
+ # Possible values are 'sample', 'full', 'nonstop'.
66
+ def expression
67
+ return @expression
68
+ end
69
+
70
+ # Sets the expression attribute on the enclosure.
71
+ # Allowed values are 'sample', 'full', 'nonstop'.
72
+ def expression=(new_expression)
73
+ unless ['sample', 'full', 'nonstop'].include? new_expression.downcase
74
+ raise ArgumentError,
75
+ "Permitted values are 'sample', 'full', 'nonstop'."
76
+ end
77
+ @expression = new_expression.downcase
78
+ end
79
+
80
+ # Returns true if this enclosure contains audio content
81
+ def audio?
82
+ unless self.type.nil?
83
+ return true if (self.type =~ /^audio/) != nil
84
+ end
85
+ # TODO: create a more complete list
86
+ # =================================
87
+ audio_extensions = ['mp3', 'm4a', 'm4p', 'wav', 'ogg', 'wma']
88
+ audio_extensions.each do |extension|
89
+ if (url =~ /#{extension}$/) != nil
90
+ return true
91
+ end
92
+ end
93
+ return false
94
+ end
95
+
96
+ # Returns true if this enclosure contains video content
97
+ def video?
98
+ unless self.type.nil?
99
+ return true if (self.type =~ /^video/) != nil
100
+ return true if self.type == "image/mov"
101
+ end
102
+ # TODO: create a more complete list
103
+ # =================================
104
+ video_extensions = ['mov', 'mp4', 'avi', 'wmv', 'asf']
105
+ video_extensions.each do |extension|
106
+ if (url =~ /#{extension}$/) != nil
107
+ return true
108
+ end
109
+ end
110
+ return false
111
+ end
112
+
113
+ alias_method :link, :url
114
+ alias_method :link=, :url=
115
+ end
116
+
117
+ # TODO: Make these actual classes instead of structs
118
+ # ==================================================
119
+ EnclosureHash = Struct.new( "EnclosureHash", :hash, :type )
120
+ EnclosurePlayer = Struct.new( "EnclosurePlayer", :url, :height, :width )
121
+ EnclosureCredit = Struct.new( "EnclosureCredit", :name, :role )
122
+ EnclosureThumbnail = Struct.new( "EnclosureThumbnail", :url, :height,
123
+ :width )
124
+
125
+ # Initialize the feed object
126
+ def initialize
127
+ super
128
+ @feed = nil
129
+ @xml_doc = nil
130
+ @root_node = nil
131
+ @title = nil
132
+ @id = nil
133
+ @time = nil
134
+ end
135
+
136
+ # Returns the parent feed of this feed item
137
+ def feed
138
+ return @feed
139
+ end
140
+
141
+ # Sets the parent feed of this feed item
142
+ def feed=(new_feed)
143
+ @feed = new_feed
144
+ end
145
+
146
+ # Returns the feed item's raw xml data.
147
+ def xml_data
148
+ return @xml_data
149
+ end
150
+
151
+ # Sets the feed item's xml data.
152
+ def xml_data=(new_xml_data)
153
+ @xml_data = new_xml_data
154
+ end
155
+
156
+ # Returns a REXML Document of the xml_data
157
+ def xml
158
+ if @xml_doc.nil?
159
+ # TODO: :ignore_whitespace_nodes => :all
160
+ # Add that?
161
+ # ======================================
162
+ @xml_doc = Document.new(xml_data)
163
+ end
164
+ return @xml_doc
165
+ end
166
+
167
+ # Returns the first node within the root_node that matches the xpath query.
168
+ def find_node(xpath)
169
+ return XPath.first(root_node, xpath)
170
+ end
171
+
172
+ # Returns all nodes within the root_node that match the xpath query.
173
+ def find_all_nodes(xpath)
174
+ return XPath.match(root_node, xpath)
175
+ end
176
+
177
+ # Returns the root node of the feed item.
178
+ def root_node
179
+ if @root_node.nil?
180
+ @root_node = xml.root
181
+ end
182
+ return @root_node
183
+ end
184
+
185
+ # Returns the feed items's unique id
186
+ def id
187
+ if @id.nil?
188
+ unless root_node.nil?
189
+ @id = XPath.first(root_node, "id/text()").to_s
190
+ if @id == ""
191
+ @id = XPath.first(root_node, "guid/text()").to_s
192
+ end
193
+ end
194
+ @id = nil if @id == ""
195
+ end
196
+ return @id
197
+ end
198
+
199
+ # Sets the feed item's unique id
200
+ def id=(new_id)
201
+ @id = new_id
202
+ end
203
+
204
+ # Returns the feed item title
205
+ def title
206
+ if @title.nil?
207
+ unless root_node.nil?
208
+ repair_entities = false
209
+ title_node = XPath.first(root_node, "title")
210
+ if title_node.nil?
211
+ title_node = XPath.first(root_node, "dc:title")
212
+ end
213
+ if title_node.nil?
214
+ title_node = XPath.first(root_node, "TITLE")
215
+ end
216
+ end
217
+ if title_node.nil?
218
+ return nil
219
+ end
220
+ if XPath.first(title_node, "@type").to_s == "xhtml" ||
221
+ XPath.first(title_node, "@mode").to_s == "xhtml" ||
222
+ XPath.first(title_node, "@type").to_s == "xml" ||
223
+ XPath.first(title_node, "@mode").to_s == "xml" ||
224
+ XPath.first(title_node, "@type").to_s == "application/xhtml+xml"
225
+ @title = title_node.inner_xml
226
+ elsif XPath.first(title_node, "@type").to_s == "escaped" ||
227
+ XPath.first(title_node, "@mode").to_s == "escaped"
228
+ @title = FeedTools.unescape_entities(
229
+ XPath.first(title_node, "text()").to_s)
230
+ else
231
+ @title = title_node.inner_xml
232
+ repair_entities = true
233
+ end
234
+ unless @title.nil?
235
+ @title = FeedTools.sanitize_html(@title, :strip)
236
+ @title = FeedTools.unescape_entities(@title) if repair_entities
237
+ @title = FeedTools.tidy_html(@title)
238
+ end
239
+ if @title != ""
240
+ # Some blogging tools include the number of comments in a post
241
+ # in the title... this is supremely ugly, and breaks any
242
+ # applications which expect the title to be static, so we're
243
+ # gonna strip them out.
244
+ #
245
+ # If for some incredibly wierd reason you need the actual
246
+ # unstripped title, just use find_node("title/text()").to_s
247
+ @title = @title.strip.gsub(/\[\d*\]$/, "").strip
248
+ end
249
+ @title.gsub!(/\n/, " ")
250
+ @title.strip!
251
+ @title = nil if @title == ""
252
+ end
253
+ return @title
254
+ end
255
+
256
+ # Sets the feed item title
257
+ def title=(new_title)
258
+ @title = new_title
259
+ end
260
+
261
+ # Returns the feed item description
262
+ def description
263
+ if @description.nil?
264
+ unless root_node.nil?
265
+ repair_entities = false
266
+ description_node = XPath.first(root_node, "content:encoded")
267
+ if description_node.nil?
268
+ description_node = XPath.first(root_node, "content")
269
+ end
270
+ if description_node.nil?
271
+ description_node = XPath.first(root_node, "fullitem")
272
+ end
273
+ if description_node.nil?
274
+ description_node = XPath.first(root_node, "xhtml:body")
275
+ end
276
+ if description_node.nil?
277
+ description_node = XPath.first(root_node, "body")
278
+ end
279
+ if description_node.nil?
280
+ description_node = XPath.first(root_node, "description")
281
+ end
282
+ if description_node.nil?
283
+ description_node = XPath.first(root_node, "tagline")
284
+ end
285
+ if description_node.nil?
286
+ description_node = XPath.first(root_node, "subtitle")
287
+ end
288
+ if description_node.nil?
289
+ description_node = XPath.first(root_node, "summary")
290
+ end
291
+ if description_node.nil?
292
+ description_node = XPath.first(root_node, "abstract")
293
+ end
294
+ if description_node.nil?
295
+ description_node = XPath.first(root_node, "ABSTRACT")
296
+ end
297
+ if description_node.nil?
298
+ description_node = XPath.first(root_node, "info")
299
+ @bozo = true unless description_node.nil?
300
+ end
301
+ end
302
+ if description_node.nil?
303
+ return nil
304
+ end
305
+ unless description_node.nil?
306
+ if XPath.first(description_node, "@encoding").to_s != ""
307
+ @description =
308
+ "[Embedded data objects are not currently supported.]"
309
+ elsif XPath.first(description_node, "@type").to_s == "xhtml" ||
310
+ XPath.first(description_node, "@mode").to_s == "xhtml" ||
311
+ XPath.first(description_node, "@type").to_s == "xml" ||
312
+ XPath.first(description_node, "@mode").to_s == "xml" ||
313
+ XPath.first(description_node, "@type").to_s ==
314
+ "application/xhtml+xml"
315
+ @description = description_node.inner_xml
316
+ elsif XPath.first(description_node, "@type").to_s == "escaped" ||
317
+ XPath.first(description_node, "@mode").to_s == "escaped"
318
+ @description = FeedTools.unescape_entities(
319
+ description_node.inner_xml)
320
+ else
321
+ @description = description_node.inner_xml
322
+ repair_entities = true
323
+ end
324
+ end
325
+ if @description == ""
326
+ @description = self.itunes_summary
327
+ @description = "" if @description.nil?
328
+ end
329
+ if @description == ""
330
+ @description = self.itunes_subtitle
331
+ @description = "" if @description.nil?
332
+ end
333
+
334
+ unless @description.nil?
335
+ @description = FeedTools.sanitize_html(@description, :strip)
336
+ @description = FeedTools.unescape_entities(@description) if repair_entities
337
+ @description = FeedTools.tidy_html(@description)
338
+ end
339
+
340
+ @description = @description.strip unless @description.nil?
341
+ @description = nil if @description == ""
342
+ end
343
+ return @description
344
+ end
345
+
346
+ # Sets the feed item description
347
+ def description=(new_description)
348
+ @description = new_description
349
+ end
350
+
351
+ # Returns the contents of the itunes:summary element
352
+ def itunes_summary
353
+ if @itunes_summary.nil?
354
+ @itunes_summary = FeedTools.unescape_entities(XPath.first(root_node,
355
+ "itunes:summary/text()").to_s)
356
+ if @itunes_summary == ""
357
+ @itunes_summary = nil
358
+ end
359
+ unless @itunes_summary.nil?
360
+ @itunes_summary = FeedTools.sanitize_html(@itunes_summary)
361
+ end
362
+ end
363
+ return @itunes_summary
364
+ end
365
+
366
+ # Sets the contents of the itunes:summary element
367
+ def itunes_summary=(new_itunes_summary)
368
+ @itunes_summary = new_itunes_summary
369
+ end
370
+
371
+ # Returns the contents of the itunes:subtitle element
372
+ def itunes_subtitle
373
+ if @itunes_subtitle.nil?
374
+ @itunes_subtitle = FeedTools.unescape_entities(XPath.first(root_node,
375
+ "itunes:subtitle/text()").to_s)
376
+ if @itunes_subtitle == ""
377
+ @itunes_subtitle = nil
378
+ end
379
+ unless @itunes_subtitle.nil?
380
+ @itunes_subtitle = FeedTools.sanitize_html(@itunes_subtitle)
381
+ end
382
+ end
383
+ return @itunes_subtitle
384
+ end
385
+
386
+ # Sets the contents of the itunes:subtitle element
387
+ def itunes_subtitle=(new_itunes_subtitle)
388
+ @itunes_subtitle = new_itunes_subtitle
389
+ end
390
+
391
+ # Returns the contents of the media:text element
392
+ def media_text
393
+ if @media_text.nil?
394
+ @media_text = FeedTools.unescape_entities(XPath.first(root_node,
395
+ "itunes:subtitle/text()").to_s)
396
+ if @media_text == ""
397
+ @media_text = nil
398
+ end
399
+ unless @media_text.nil?
400
+ @media_text = FeedTools.sanitize_html(@media_text)
401
+ end
402
+ end
403
+ return @media_text
404
+ end
405
+
406
+ # Sets the contents of the media:text element
407
+ def media_text=(new_media_text)
408
+ @media_text = new_media_text
409
+ end
410
+
411
+ # Returns the feed item link
412
+ def link
413
+ if @link.nil?
414
+ unless root_node.nil?
415
+ @link = XPath.first(root_node, "link[@rel='alternate']/@href").to_s
416
+ if @link == ""
417
+ @link = XPath.first(root_node, "link/@href").to_s
418
+ end
419
+ if @link == ""
420
+ @link = XPath.first(root_node, "link/text()").to_s
421
+ end
422
+ if @link == ""
423
+ @link = XPath.first(root_node, "@rdf:about").to_s
424
+ end
425
+ if @link == ""
426
+ @link = XPath.first(root_node, "guid[@isPermaLink='true']/text()").to_s
427
+ end
428
+ if @link == ""
429
+ @link = XPath.first(root_node, "@href").to_s
430
+ end
431
+ if @link == ""
432
+ @link = XPath.first(root_node, "a/@href").to_s
433
+ end
434
+ if @link == ""
435
+ @link = XPath.first(root_node, "@HREF").to_s
436
+ end
437
+ if @link == ""
438
+ @link = XPath.first(root_node, "A/@HREF").to_s
439
+ end
440
+ end
441
+ if @link == "" || @link.nil?
442
+ if FeedTools.is_uri? self.guid
443
+ @link = self.guid
444
+ end
445
+ end
446
+ if @link != ""
447
+ @link = FeedTools.unescape_entities(@link)
448
+ end
449
+ # TODO: Actually implement proper relative url resolving instead of this crap
450
+ # ===========================================================================
451
+ #
452
+ # if @link != "" && (@link =~ /http:\/\//) != 0 && (@link =~ /https:\/\//) != 0
453
+ # if (feed.base[-1..-1] == "/" && @link[0..0] == "/")
454
+ # @link = @link[1..-1]
455
+ # end
456
+ # # prepend the base to the link since they seem to have used a relative path
457
+ # @link = feed.base + @link
458
+ # end
459
+ @link = FeedTools.normalize_url(@link)
460
+ end
461
+ return @link
462
+ end
463
+
464
+ # Sets the feed item link
465
+ def link=(new_link)
466
+ @link = new_link
467
+ end
468
+
469
+ # Returns a list of the feed item's categories
470
+ def categories
471
+ if @categories.nil?
472
+ @categories = []
473
+ category_nodes = XPath.match(root_node, "category")
474
+ if category_nodes.nil? || category_nodes.empty?
475
+ category_nodes = XPath.match(root_node, "dc:subject")
476
+ end
477
+ unless category_nodes.nil?
478
+ for category_node in category_nodes
479
+ category = FeedTools::Feed::Category.new
480
+ category.term = XPath.first(category_node, "@term").to_s
481
+ if category.term == ""
482
+ category.term = XPath.first(category_node, "text()").to_s
483
+ end
484
+ category.term.strip! unless category.term.nil?
485
+ category.term = nil if category.term == ""
486
+ category.label = XPath.first(category_node, "@label").to_s
487
+ category.label.strip! unless category.label.nil?
488
+ category.label = nil if category.label == ""
489
+ category.scheme = XPath.first(category_node, "@scheme").to_s
490
+ if category.scheme == ""
491
+ category.scheme = XPath.first(category_node, "@domain").to_s
492
+ end
493
+ category.scheme.strip! unless category.scheme.nil?
494
+ category.scheme = nil if category.scheme == ""
495
+ @categories << category
496
+ end
497
+ end
498
+ end
499
+ return @categories
500
+ end
501
+
502
+ # Returns a list of the feed items's images
503
+ def images
504
+ if @images.nil?
505
+ @images = []
506
+ image_nodes = XPath.match(root_node, "link")
507
+ if image_nodes.nil? || image_nodes.empty?
508
+ image_nodes = XPath.match(root_node, "logo")
509
+ end
510
+ if image_nodes.nil? || image_nodes.empty?
511
+ image_nodes = XPath.match(root_node, "LOGO")
512
+ end
513
+ if image_nodes.nil? || image_nodes.empty?
514
+ image_nodes = XPath.match(root_node, "image")
515
+ end
516
+ unless image_nodes.nil?
517
+ for image_node in image_nodes
518
+ image = FeedTools::Feed::Image.new
519
+ image.url = XPath.first(image_node, "url/text()").to_s
520
+ if image.url != ""
521
+ self.feed.bozo = true
522
+ end
523
+ if image.url == ""
524
+ image.url = XPath.first(image_node, "@rdf:resource").to_s
525
+ end
526
+ if image.url == "" && (image_node.name == "logo" ||
527
+ (image_node.attributes['type'] =~ /^image/) == 0)
528
+ image.url = XPath.first(image_node, "@href").to_s
529
+ end
530
+ if image.url == "" && image_node.name == "LOGO"
531
+ image.url = XPath.first(image_node, "@HREF").to_s
532
+ end
533
+ image.url.strip! unless image.url.nil?
534
+ image.url = nil if image.url == ""
535
+ image.title = XPath.first(image_node, "title/text()").to_s
536
+ image.title.strip! unless image.title.nil?
537
+ image.title = nil if image.title == ""
538
+ image.description =
539
+ XPath.first(image_node, "description/text()").to_s
540
+ image.description.strip! unless image.description.nil?
541
+ image.description = nil if image.description == ""
542
+ image.link = XPath.first(image_node, "link/text()").to_s
543
+ image.link.strip! unless image.link.nil?
544
+ image.link = nil if image.link == ""
545
+ image.height = XPath.first(image_node, "height/text()").to_s.to_i
546
+ image.height = nil if image.height <= 0
547
+ image.width = XPath.first(image_node, "width/text()").to_s.to_i
548
+ image.width = nil if image.width <= 0
549
+ image.style = XPath.first(image_node, "@style").to_s.downcase
550
+ if image.style == ""
551
+ image.style = XPath.first(image_node, "@STYLE").to_s.downcase
552
+ end
553
+ image.style.strip! unless image.style.nil?
554
+ image.style = nil if image.style == ""
555
+ @images << image
556
+ end
557
+ end
558
+ end
559
+ return @images
560
+ end
561
+
562
+ # Returns the feed item itunes image link
563
+ #
564
+ # If it's not present, falls back to the normal image link.
565
+ # Technically, the itunes spec says that the image needs to be
566
+ # square and larger than 300x300, but hey, if there's an image
567
+ # to be had, it's better than none at all.
568
+ def itunes_image_link
569
+ if @itunes_image_link.nil?
570
+ # get the feed item itunes image link from the xml document
571
+ @itunes_image_link = XPath.first(root_node, "itunes:image/@href").to_s
572
+ if @itunes_image_link == ""
573
+ @itunes_image_link = XPath.first(root_node, "itunes:link[@rel='image']/@href").to_s
574
+ end
575
+ @itunes_image_link = FeedTools.normalize_url(@itunes_image_link)
576
+ end
577
+ return @itunes_image_link
578
+ end
579
+
580
+ # Sets the feed item itunes image link
581
+ def itunes_image_link=(new_itunes_image_link)
582
+ @itunes_image_link = new_itunes_image_link
583
+ end
584
+
585
+ # Returns the feed item media thumbnail link
586
+ #
587
+ # If it's not present, falls back to the normal image link.
588
+ def media_thumbnail_link
589
+ if @media_thumbnail_link.nil?
590
+ # get the feed item itunes image link from the xml document
591
+ @media_thumbnail_link = XPath.first(root_node, "media:thumbnail/@url").to_s
592
+ @media_thumbnail_link = FeedTools.normalize_url(@media_thumbnail_link)
593
+ end
594
+ return @media_thumbnail_link
595
+ end
596
+
597
+ # Sets the feed item media thumbnail url
598
+ def media_thumbnail_link=(new_media_thumbnail_link)
599
+ @media_thumbnail_link = new_media_thumbnail_link
600
+ end
601
+
602
+ # Returns the feed item's copyright information
603
+ def copyright
604
+ if @copyright.nil?
605
+ unless root_node.nil?
606
+ @copyright = XPath.first(root_node, "dc:rights/text()").to_s
607
+ if @copyright == ""
608
+ @copyright = XPath.first(root_node, "rights/text()").to_s
609
+ end
610
+ if @copyright == ""
611
+ @copyright = XPath.first(root_node, "copyright/text()").to_s
612
+ end
613
+ if @copyright == ""
614
+ @copyright = XPath.first(root_node, "copyrights/text()").to_s
615
+ end
616
+ @copyright = FeedTools.sanitize_html(@copyright, :strip)
617
+ @copyright = nil if @copyright == ""
618
+ end
619
+ end
620
+ return @copyright
621
+ end
622
+
623
+ # Sets the feed item's copyright information
624
+ def copyright=(new_copyright)
625
+ @copyright = new_copyright
626
+ end
627
+
628
+ # Returns all feed item enclosures
629
+ def enclosures
630
+ if @enclosures.nil?
631
+ @enclosures = []
632
+
633
+ # First, load up all the different possible sources of enclosures
634
+ rss_enclosures = XPath.match(root_node, "enclosure")
635
+ atom_enclosures = XPath.match(root_node, "link[@rel='enclosure']")
636
+ media_content_enclosures = XPath.match(root_node, "media:content")
637
+ media_group_enclosures = XPath.match(root_node, "media:group")
638
+
639
+ # Parse RSS-type enclosures. Thanks to a few buggy enclosures implementations,
640
+ # sometimes these also manage to show up in atom files.
641
+ for enclosure_node in rss_enclosures
642
+ enclosure = Enclosure.new
643
+ enclosure.url = FeedTools.unescape_entities(enclosure_node.attributes["url"].to_s)
644
+ enclosure.type = enclosure_node.attributes["type"].to_s
645
+ enclosure.file_size = enclosure_node.attributes["length"].to_i
646
+ enclosure.credits = []
647
+ enclosure.explicit = false
648
+ @enclosures << enclosure
649
+ end
650
+
651
+ # Parse atom-type enclosures. If there are repeats of the same enclosure object,
652
+ # we merge the two together.
653
+ for enclosure_node in atom_enclosures
654
+ enclosure_url = FeedTools.unescape_entities(enclosure_node.attributes["href"].to_s)
655
+ enclosure = nil
656
+ new_enclosure = false
657
+ for existing_enclosure in @enclosures
658
+ if existing_enclosure.url == enclosure_url
659
+ enclosure = existing_enclosure
660
+ break
661
+ end
662
+ end
663
+ if enclosure.nil?
664
+ new_enclosure = true
665
+ enclosure = Enclosure.new
666
+ end
667
+ enclosure.url = enclosure_url
668
+ enclosure.type = enclosure_node.attributes["type"].to_s
669
+ enclosure.file_size = enclosure_node.attributes["length"].to_i
670
+ enclosure.credits = []
671
+ enclosure.explicit = false
672
+ if new_enclosure
673
+ @enclosures << enclosure
674
+ end
675
+ end
676
+
677
+ # Creates an anonymous method to parse content objects from the media module. We
678
+ # do this to avoid excessive duplication of code since we have to do identical
679
+ # processing for content objects within group objects.
680
+ parse_media_content = lambda do |media_content_nodes|
681
+ affected_enclosures = []
682
+ for enclosure_node in media_content_nodes
683
+ enclosure_url = FeedTools.unescape_entities(enclosure_node.attributes["url"].to_s)
684
+ enclosure = nil
685
+ new_enclosure = false
686
+ for existing_enclosure in @enclosures
687
+ if existing_enclosure.url == enclosure_url
688
+ enclosure = existing_enclosure
689
+ break
690
+ end
691
+ end
692
+ if enclosure.nil?
693
+ new_enclosure = true
694
+ enclosure = Enclosure.new
695
+ end
696
+ enclosure.url = enclosure_url
697
+ enclosure.type = enclosure_node.attributes["type"].to_s
698
+ enclosure.file_size = enclosure_node.attributes["fileSize"].to_i
699
+ enclosure.duration = enclosure_node.attributes["duration"].to_s
700
+ enclosure.height = enclosure_node.attributes["height"].to_i
701
+ enclosure.width = enclosure_node.attributes["width"].to_i
702
+ enclosure.bitrate = enclosure_node.attributes["bitrate"].to_i
703
+ enclosure.framerate = enclosure_node.attributes["framerate"].to_i
704
+ enclosure.expression = enclosure_node.attributes["expression"].to_s
705
+ enclosure.is_default =
706
+ (enclosure_node.attributes["isDefault"].to_s.downcase == "true")
707
+ if XPath.first(enclosure_node, "media:thumbnail/@url").to_s != ""
708
+ enclosure.thumbnail = EnclosureThumbnail.new(
709
+ FeedTools.unescape_entities(XPath.first(enclosure_node, "media:thumbnail/@url").to_s),
710
+ FeedTools.unescape_entities(XPath.first(enclosure_node, "media:thumbnail/@height").to_s),
711
+ FeedTools.unescape_entities(XPath.first(enclosure_node, "media:thumbnail/@width").to_s)
712
+ )
713
+ if enclosure.thumbnail.height == ""
714
+ enclosure.thumbnail.height = nil
715
+ end
716
+ if enclosure.thumbnail.width == ""
717
+ enclosure.thumbnail.width = nil
718
+ end
719
+ end
720
+ enclosure.categories = []
721
+ for category in XPath.match(enclosure_node, "media:category")
722
+ enclosure.categories << FeedTools::Feed::Category.new
723
+ enclosure.categories.last.term =
724
+ FeedTools.unescape_entities(category.text)
725
+ enclosure.categories.last.scheme =
726
+ FeedTools.unescape_entities(category.attributes["scheme"].to_s)
727
+ enclosure.categories.last.label =
728
+ FeedTools.unescape_entities(category.attributes["label"].to_s)
729
+ if enclosure.categories.last.scheme == ""
730
+ enclosure.categories.last.scheme = nil
731
+ end
732
+ if enclosure.categories.last.label == ""
733
+ enclosure.categories.last.label = nil
734
+ end
735
+ end
736
+ if XPath.first(enclosure_node, "media:hash/text()").to_s != ""
737
+ enclosure.hash = EnclosureHash.new(
738
+ FeedTools.sanitize_html(FeedTools.unescape_entities(XPath.first(
739
+ enclosure_node, "media:hash/text()").to_s), :strip),
740
+ "md5"
741
+ )
742
+ end
743
+ if XPath.first(enclosure_node, "media:player/@url").to_s != ""
744
+ enclosure.player = EnclosurePlayer.new(
745
+ FeedTools.unescape_entities(XPath.first(enclosure_node, "media:player/@url").to_s),
746
+ FeedTools.unescape_entities(XPath.first(enclosure_node, "media:player/@height").to_s),
747
+ FeedTools.unescape_entities(XPath.first(enclosure_node, "media:player/@width").to_s)
748
+ )
749
+ if enclosure.player.height == ""
750
+ enclosure.player.height = nil
751
+ end
752
+ if enclosure.player.width == ""
753
+ enclosure.player.width = nil
754
+ end
755
+ end
756
+ enclosure.credits = []
757
+ for credit in XPath.match(enclosure_node, "media:credit")
758
+ enclosure.credits << EnclosureCredit.new(
759
+ FeedTools.unescape_entities(credit.text),
760
+ FeedTools.unescape_entities(credit.attributes["role"].to_s.downcase)
761
+ )
762
+ if enclosure.credits.last.role == ""
763
+ enclosure.credits.last.role = nil
764
+ end
765
+ end
766
+ enclosure.explicit = (XPath.first(enclosure_node,
767
+ "media:adult/text()").to_s.downcase == "true")
768
+ if XPath.first(enclosure_node, "media:text/text()").to_s != ""
769
+ enclosure.text = FeedTools.unescape_entities(XPath.first(enclosure_node,
770
+ "media:text/text()").to_s)
771
+ end
772
+ affected_enclosures << enclosure
773
+ if new_enclosure
774
+ @enclosures << enclosure
775
+ end
776
+ end
777
+ affected_enclosures
778
+ end
779
+
780
+ # Parse the independant content objects.
781
+ parse_media_content.call(media_content_enclosures)
782
+
783
+ media_groups = []
784
+
785
+ # Parse the group objects.
786
+ for media_group in media_group_enclosures
787
+ group_media_content_enclosures =
788
+ XPath.match(media_group, "media:content")
789
+
790
+ # Parse the content objects within the group objects.
791
+ affected_enclosures =
792
+ parse_media_content.call(group_media_content_enclosures)
793
+
794
+ # Now make sure that content objects inherit certain properties from
795
+ # the group objects.
796
+ for enclosure in affected_enclosures
797
+ if enclosure.thumbnail.nil? &&
798
+ XPath.first(media_group, "media:thumbnail/@url").to_s != ""
799
+ enclosure.thumbnail = EnclosureThumbnail.new(
800
+ FeedTools.unescape_entities(
801
+ XPath.first(media_group, "media:thumbnail/@url").to_s),
802
+ FeedTools.unescape_entities(
803
+ XPath.first(media_group, "media:thumbnail/@height").to_s),
804
+ FeedTools.unescape_entities(
805
+ XPath.first(media_group, "media:thumbnail/@width").to_s)
806
+ )
807
+ if enclosure.thumbnail.height == ""
808
+ enclosure.thumbnail.height = nil
809
+ end
810
+ if enclosure.thumbnail.width == ""
811
+ enclosure.thumbnail.width = nil
812
+ end
813
+ end
814
+ if (enclosure.categories.nil? || enclosure.categories.size == 0)
815
+ enclosure.categories = []
816
+ for category in XPath.match(media_group, "media:category")
817
+ enclosure.categories << FeedTools::Feed::Category.new
818
+ enclosure.categories.last.term =
819
+ FeedTools.unescape_entities(category.text)
820
+ enclosure.categories.last.scheme =
821
+ FeedTools.unescape_entities(category.attributes["scheme"].to_s)
822
+ enclosure.categories.last.label =
823
+ FeedTools.unescape_entities(category.attributes["label"].to_s)
824
+ if enclosure.categories.last.scheme == ""
825
+ enclosure.categories.last.scheme = nil
826
+ end
827
+ if enclosure.categories.last.label == ""
828
+ enclosure.categories.last.label = nil
829
+ end
830
+ end
831
+ end
832
+ if enclosure.hash.nil? &&
833
+ XPath.first(media_group, "media:hash/text()").to_s != ""
834
+ enclosure.hash = EnclosureHash.new(
835
+ FeedTools.unescape_entities(XPath.first(media_group, "media:hash/text()").to_s),
836
+ "md5"
837
+ )
838
+ end
839
+ if enclosure.player.nil? &&
840
+ XPath.first(media_group, "media:player/@url").to_s != ""
841
+ enclosure.player = EnclosurePlayer.new(
842
+ FeedTools.unescape_entities(XPath.first(media_group, "media:player/@url").to_s),
843
+ FeedTools.unescape_entities(XPath.first(media_group, "media:player/@height").to_s),
844
+ FeedTools.unescape_entities(XPath.first(media_group, "media:player/@width").to_s)
845
+ )
846
+ if enclosure.player.height == ""
847
+ enclosure.player.height = nil
848
+ end
849
+ if enclosure.player.width == ""
850
+ enclosure.player.width = nil
851
+ end
852
+ end
853
+ if enclosure.credits.nil? || enclosure.credits.size == 0
854
+ enclosure.credits = []
855
+ for credit in XPath.match(media_group, "media:credit")
856
+ enclosure.credits << EnclosureCredit.new(
857
+ FeedTools.unescape_entities(credit.text),
858
+ FeedTools.unescape_entities(credit.attributes["role"].to_s.downcase)
859
+ )
860
+ if enclosure.credits.last.role == ""
861
+ enclosure.credits.last.role = nil
862
+ end
863
+ end
864
+ end
865
+ if enclosure.explicit?.nil?
866
+ enclosure.explicit = (XPath.first(media_group,
867
+ "media:adult/text()").to_s.downcase == "true") ? true : false
868
+ end
869
+ if enclosure.text.nil? &&
870
+ XPath.first(media_group, "media:text/text()").to_s != ""
871
+ enclosure.text = FeedTools.sanitize_html(FeedTools.unescape_entities(
872
+ XPath.first(media_group, "media:text/text()").to_s), :strip)
873
+ end
874
+ end
875
+
876
+ # Keep track of the media groups
877
+ media_groups << affected_enclosures
878
+ end
879
+
880
+ # Now we need to inherit any relevant item level information.
881
+ if self.explicit?
882
+ for enclosure in @enclosures
883
+ enclosure.explicit = true
884
+ end
885
+ end
886
+
887
+ # Add all the itunes categories
888
+ for itunes_category in XPath.match(root_node, "itunes:category")
889
+ genre = "Podcasts"
890
+ category = itunes_category.attributes["text"].to_s
891
+ subcategory = XPath.first(itunes_category, "itunes:category/@text").to_s
892
+ category_path = genre
893
+ if category != ""
894
+ category_path << "/" + category
895
+ end
896
+ if subcategory != ""
897
+ category_path << "/" + subcategory
898
+ end
899
+ for enclosure in @enclosures
900
+ if enclosure.categories.nil?
901
+ enclosure.categories = []
902
+ end
903
+ enclosure.categories << EnclosureCategory.new(
904
+ FeedTools.unescape_entities(category_path),
905
+ FeedTools.unescape_entities("http://www.apple.com/itunes/store/"),
906
+ FeedTools.unescape_entities("iTunes Music Store Categories")
907
+ )
908
+ end
909
+ end
910
+
911
+ for enclosure in @enclosures
912
+ # Clean up any of those attributes that incorrectly have ""
913
+ # or 0 as their values
914
+ if enclosure.type == ""
915
+ enclosure.type = nil
916
+ end
917
+ if enclosure.file_size == 0
918
+ enclosure.file_size = nil
919
+ end
920
+ if enclosure.duration == 0
921
+ enclosure.duration = nil
922
+ end
923
+ if enclosure.height == 0
924
+ enclosure.height = nil
925
+ end
926
+ if enclosure.width == 0
927
+ enclosure.width = nil
928
+ end
929
+ if enclosure.bitrate == 0
930
+ enclosure.bitrate = nil
931
+ end
932
+ if enclosure.framerate == 0
933
+ enclosure.framerate = nil
934
+ end
935
+ if enclosure.expression == "" || enclosure.expression.nil?
936
+ enclosure.expression = "full"
937
+ end
938
+
939
+ # If an enclosure is missing the text field, fall back on the itunes:summary field
940
+ if enclosure.text.nil? || enclosure.text = ""
941
+ enclosure.text = self.itunes_summary
942
+ end
943
+
944
+ # Make sure we don't have duplicate categories
945
+ unless enclosure.categories.nil?
946
+ enclosure.categories.uniq!
947
+ end
948
+ end
949
+
950
+ # And finally, now things get complicated. This is where we make
951
+ # sure that the enclosures method only returns either default
952
+ # enclosures or enclosures with only one version. Any enclosures
953
+ # that are wrapped in a media:group will be placed in the appropriate
954
+ # versions field.
955
+ affected_enclosure_urls = []
956
+ for media_group in media_groups
957
+ affected_enclosure_urls =
958
+ affected_enclosure_urls | (media_group.map do |enclosure|
959
+ enclosure.url
960
+ end)
961
+ end
962
+ @enclosures.delete_if do |enclosure|
963
+ (affected_enclosure_urls.include? enclosure.url)
964
+ end
965
+ for media_group in media_groups
966
+ default_enclosure = nil
967
+ for enclosure in media_group
968
+ if enclosure.is_default?
969
+ default_enclosure = enclosure
970
+ end
971
+ end
972
+ for enclosure in media_group
973
+ enclosure.default_version = default_enclosure
974
+ enclosure.versions = media_group.clone
975
+ enclosure.versions.delete(enclosure)
976
+ end
977
+ @enclosures << default_enclosure
978
+ end
979
+ end
980
+
981
+ # If we have a single enclosure, it's safe to inherit the itunes:duration field
982
+ # if it's missing.
983
+ if @enclosures.size == 1
984
+ if @enclosures.first.duration.nil? || @enclosures.first.duration == 0
985
+ @enclosures.first.duration = self.itunes_duration
986
+ end
987
+ end
988
+
989
+ return @enclosures
990
+ end
991
+
992
+ def enclosures=(new_enclosures)
993
+ @enclosures = new_enclosures
994
+ end
995
+
996
+ # Returns the feed item author
997
+ def author
998
+ if @author.nil?
999
+ @author = FeedTools::Feed::Author.new
1000
+ unless root_node.nil?
1001
+ author_node = XPath.first(root_node, "author")
1002
+ if author_node.nil?
1003
+ author_node = XPath.first(root_node, "managingEditor")
1004
+ end
1005
+ if author_node.nil?
1006
+ author_node = XPath.first(root_node, "dc:author")
1007
+ end
1008
+ if author_node.nil?
1009
+ author_node = XPath.first(root_node, "dc:creator")
1010
+ end
1011
+ if author_node.nil?
1012
+ author_node = XPath.first(root_node, "atom:author")
1013
+ end
1014
+ end
1015
+ unless author_node.nil?
1016
+ @author.raw = FeedTools.unescape_entities(
1017
+ XPath.first(author_node, "text()").to_s)
1018
+ @author.raw = nil if @author.raw == ""
1019
+ unless @author.raw.nil?
1020
+ raw_scan = @author.raw.scan(
1021
+ /(.*)\((\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\)/i)
1022
+ if raw_scan.nil? || raw_scan.size == 0
1023
+ raw_scan = @author.raw.scan(
1024
+ /(\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\s*\((.*)\)/i)
1025
+ author_raw_pair = raw_scan.first.reverse unless raw_scan.size == 0
1026
+ else
1027
+ author_raw_pair = raw_scan.first
1028
+ end
1029
+ if raw_scan.nil? || raw_scan.size == 0
1030
+ email_scan = @author.raw.scan(
1031
+ /\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b/i)
1032
+ if email_scan != nil && email_scan.size > 0
1033
+ @author.email = email_scan.first.strip
1034
+ end
1035
+ end
1036
+ unless author_raw_pair.nil? || author_raw_pair.size == 0
1037
+ @author.name = author_raw_pair.first.strip
1038
+ @author.email = author_raw_pair.last.strip
1039
+ else
1040
+ unless @author.raw.include?("@")
1041
+ # We can be reasonably sure we are looking at something
1042
+ # that the creator didn't intend to contain an email address if
1043
+ # it got through the preceeding regexes and it doesn't
1044
+ # contain the tell-tale '@' symbol.
1045
+ @author.name = @author.raw
1046
+ end
1047
+ end
1048
+ end
1049
+ @author.name = "" if @author.name.nil?
1050
+ if @author.name == ""
1051
+ @author.name = FeedTools.unescape_entities(
1052
+ XPath.first(author_node, "name/text()").to_s)
1053
+ end
1054
+ if @author.name == ""
1055
+ @author.name = FeedTools.unescape_entities(
1056
+ XPath.first(author_node, "@name").to_s)
1057
+ end
1058
+ if @author.email == ""
1059
+ @author.email = FeedTools.unescape_entities(
1060
+ XPath.first(author_node, "email/text()").to_s)
1061
+ end
1062
+ if @author.email == ""
1063
+ @author.email = FeedTools.unescape_entities(
1064
+ XPath.first(author_node, "@email").to_s)
1065
+ end
1066
+ if @author.url == ""
1067
+ @author.url = FeedTools.unescape_entities(
1068
+ XPath.first(author_node, "url/text()").to_s)
1069
+ end
1070
+ if @author.url == ""
1071
+ @author.url = FeedTools.unescape_entities(
1072
+ XPath.first(author_node, "@url").to_s)
1073
+ end
1074
+ @author.name = nil if @author.name == ""
1075
+ @author.raw = nil if @author.raw == ""
1076
+ @author.email = nil if @author.email == ""
1077
+ @author.url = nil if @author.url == ""
1078
+ end
1079
+ # Fallback on the itunes module if we didn't find an author name
1080
+ begin
1081
+ @author.name = self.itunes_author if @author.name.nil?
1082
+ rescue
1083
+ @author.name = nil
1084
+ end
1085
+ end
1086
+ return @author
1087
+ end
1088
+
1089
+ # Sets the feed item author
1090
+ def author=(new_author)
1091
+ if new_author.respond_to?(:name) &&
1092
+ new_author.respond_to?(:email) &&
1093
+ new_author.respond_to?(:url)
1094
+ # It's a complete author object, just set it.
1095
+ @author = new_author
1096
+ else
1097
+ # We're not looking at an author object, this is probably a string,
1098
+ # default to setting the author's name.
1099
+ if @author.nil?
1100
+ @author = FeedTools::Feed::Author.new
1101
+ end
1102
+ @author.name = new_author
1103
+ end
1104
+ end
1105
+
1106
+ # Returns the feed publisher
1107
+ def publisher
1108
+ if @publisher.nil?
1109
+ @publisher = FeedTools::Feed::Author.new
1110
+
1111
+ # Set the author name
1112
+ @publisher.raw = FeedTools.unescape_entities(
1113
+ XPath.first(root_node, "dc:publisher/text()").to_s)
1114
+ if @publisher.raw == ""
1115
+ @publisher.raw = FeedTools.unescape_entities(
1116
+ XPath.first(root_node, "webMaster/text()").to_s)
1117
+ end
1118
+ unless @publisher.raw == ""
1119
+ raw_scan = @publisher.raw.scan(
1120
+ /(.*)\((\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\)/i)
1121
+ if raw_scan.nil? || raw_scan.size == 0
1122
+ raw_scan = @publisher.raw.scan(
1123
+ /(\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\s*\((.*)\)/i)
1124
+ unless raw_scan.size == 0
1125
+ publisher_raw_pair = raw_scan.first.reverse
1126
+ end
1127
+ else
1128
+ publisher_raw_pair = raw_scan.first
1129
+ end
1130
+ if raw_scan.nil? || raw_scan.size == 0
1131
+ email_scan = @publisher.raw.scan(
1132
+ /\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b/i)
1133
+ if email_scan != nil && email_scan.size > 0
1134
+ @publisher.email = email_scan.first.strip
1135
+ end
1136
+ end
1137
+ unless publisher_raw_pair.nil? || publisher_raw_pair.size == 0
1138
+ @publisher.name = publisher_raw_pair.first.strip
1139
+ @publisher.email = publisher_raw_pair.last.strip
1140
+ else
1141
+ unless @publisher.raw.include?("@")
1142
+ # We can be reasonably sure we are looking at something
1143
+ # that the creator didn't intend to contain an email address if
1144
+ # it got through the preceeding regexes and it doesn't
1145
+ # contain the tell-tale '@' symbol.
1146
+ @publisher.name = @publisher.raw
1147
+ end
1148
+ end
1149
+ end
1150
+
1151
+ @publisher.name = nil if @publisher.name == ""
1152
+ @publisher.raw = nil if @publisher.raw == ""
1153
+ @publisher.email = nil if @publisher.email == ""
1154
+ @publisher.url = nil if @publisher.url == ""
1155
+ end
1156
+ return @publisher
1157
+ end
1158
+
1159
+ # Sets the feed publisher
1160
+ def publisher=(new_publisher)
1161
+ if new_publisher.respond_to?(:name) &&
1162
+ new_publisher.respond_to?(:email) &&
1163
+ new_publisher.respond_to?(:url)
1164
+ # It's a complete Author object, just set it.
1165
+ @publisher = new_publisher
1166
+ else
1167
+ # We're not looking at an Author object, this is probably a string,
1168
+ # default to setting the publisher's name.
1169
+ if @publisher.nil?
1170
+ @publisher = FeedTools::Feed::Author.new
1171
+ end
1172
+ @publisher.name = new_publisher
1173
+ end
1174
+ end
1175
+
1176
+ # Returns the contents of the itunes:author element
1177
+ #
1178
+ # This inherits from any incorrectly placed channel-level itunes:author
1179
+ # elements. They're actually amazingly common. People don't read specs.
1180
+ def itunes_author
1181
+ if @itunes_author.nil?
1182
+ @itunes_author = FeedTools.unescape_entities(XPath.first(root_node,
1183
+ "itunes:author/text()").to_s)
1184
+ @itunes_author = feed.itunes_author if @itunes_author == ""
1185
+ @itunes_author = nil if @itunes_author == ""
1186
+ end
1187
+ return @itunes_author
1188
+ end
1189
+
1190
+ # Sets the contents of the itunes:author element
1191
+ def itunes_author=(new_itunes_author)
1192
+ @itunes_author = new_itunes_author
1193
+ end
1194
+
1195
+ # Returns the number of seconds that the associated media runs for
1196
+ def itunes_duration
1197
+ if @itunes_duration.nil?
1198
+ raw_duration = FeedTools.unescape_entities(XPath.first(root_node,
1199
+ "itunes:duration/text()").to_s)
1200
+ if raw_duration != ""
1201
+ hms = raw_duration.split(":").map { |x| x.to_i }
1202
+ if hms.size == 3
1203
+ @itunes_duration = hms[0].hour + hms[1].minute + hms[2]
1204
+ elsif hms.size == 2
1205
+ @itunes_duration = hms[0].minute + hms[1]
1206
+ elsif hms.size == 1
1207
+ @itunes_duration = hms[0]
1208
+ end
1209
+ end
1210
+ end
1211
+ return @itunes_duration
1212
+ end
1213
+
1214
+ # Sets the number of seconds that the associate media runs for
1215
+ def itunes_duration=(new_itunes_duration)
1216
+ @itunes_duration = new_itunes_duration
1217
+ end
1218
+
1219
+ # Returns the feed item time
1220
+ def time
1221
+ if @time.nil?
1222
+ unless root_node.nil?
1223
+ time_string = XPath.first(root_node, "pubDate/text()").to_s
1224
+ if time_string == ""
1225
+ time_string = XPath.first(root_node, "dc:date/text()").to_s
1226
+ end
1227
+ if time_string == ""
1228
+ time_string = XPath.first(root_node, "issued/text()").to_s
1229
+ end
1230
+ if time_string == ""
1231
+ time_string = XPath.first(root_node, "updated/text()").to_s
1232
+ end
1233
+ if time_string == ""
1234
+ time_string = XPath.first(root_node, "time/text()").to_s
1235
+ end
1236
+ end
1237
+ if time_string != nil && time_string != ""
1238
+ @time = Time.parse(time_string) rescue Time.now
1239
+ elsif time_string == nil
1240
+ @time = Time.now
1241
+ end
1242
+ end
1243
+ return @time
1244
+ end
1245
+
1246
+ # Sets the feed item time
1247
+ def time=(new_time)
1248
+ @time = new_time
1249
+ end
1250
+
1251
+ # Returns the feed item updated time
1252
+ def updated
1253
+ if @updated.nil?
1254
+ unless root_node.nil?
1255
+ updated_string = XPath.first(root_node, "updated/text()").to_s
1256
+ if updated_string == ""
1257
+ updated_string = XPath.first(root_node, "modified/text()").to_s
1258
+ end
1259
+ end
1260
+ if updated_string != nil && updated_string != ""
1261
+ @updated = Time.parse(updated_string) rescue nil
1262
+ else
1263
+ @updated = nil
1264
+ end
1265
+ end
1266
+ return @updated
1267
+ end
1268
+
1269
+ # Sets the feed item updated time
1270
+ def updated=(new_updated)
1271
+ @updated = new_updated
1272
+ end
1273
+
1274
+ # Returns the feed item issued time
1275
+ def issued
1276
+ if @issued.nil?
1277
+ unless root_node.nil?
1278
+ issued_string = XPath.first(root_node, "issued/text()").to_s
1279
+ if issued_string == ""
1280
+ issued_string = XPath.first(root_node, "published/text()").to_s
1281
+ end
1282
+ if issued_string == ""
1283
+ issued_string = XPath.first(root_node, "pubDate/text()").to_s
1284
+ end
1285
+ if issued_string == ""
1286
+ issued_string = XPath.first(root_node, "dc:date/text()").to_s
1287
+ end
1288
+ end
1289
+ if issued_string != nil && issued_string != ""
1290
+ @issued = Time.parse(issued_string) rescue nil
1291
+ else
1292
+ @issued = nil
1293
+ end
1294
+ end
1295
+ return @issued
1296
+ end
1297
+
1298
+ # Sets the feed item issued time
1299
+ def issued=(new_issued)
1300
+ @issued = new_issued
1301
+ end
1302
+
1303
+ # Returns the url for posting comments
1304
+ def comments
1305
+ if @comments.nil?
1306
+ @comments = FeedTools.normalize_url(
1307
+ XPath.first(root_node, "comments/text()").to_s)
1308
+ @comments = nil if @comments == ""
1309
+ end
1310
+ return @comments
1311
+ end
1312
+
1313
+ # Sets the url for posting comments
1314
+ def comments=(new_comments)
1315
+ @comments = new_comments
1316
+ end
1317
+
1318
+ # The source that this post was based on
1319
+ def source
1320
+ if @source.nil?
1321
+ @source = FeedTools::Feed::Link.new
1322
+ @source.url = XPath.first(root_node, "source/@url").to_s
1323
+ @source.url = nil if @source.url == ""
1324
+ @source.value = XPath.first(root_node, "source/text()").to_s
1325
+ @source.value = nil if @source.value == ""
1326
+ end
1327
+ return @source
1328
+ end
1329
+
1330
+ # Returns the feed item tags
1331
+ def tags
1332
+ # TODO: support the rel="tag" microformat
1333
+ # =======================================
1334
+ if @tags.nil?
1335
+ @tags = []
1336
+ if @tags.nil? || @tags.size == 0
1337
+ @tags = []
1338
+ tag_list = XPath.match(root_node, "dc:subject/rdf:Bag/rdf:li/text()")
1339
+ if tag_list.size > 1
1340
+ for tag in tag_list
1341
+ @tags << tag.to_s.downcase.strip
1342
+ end
1343
+ end
1344
+ end
1345
+ if @tags.nil? || @tags.size == 0
1346
+ # messy effort to find ourselves some tags, mainly for del.icio.us
1347
+ @tags = []
1348
+ rdf_bag = XPath.match(root_node, "taxo:topics/rdf:Bag/rdf:li")
1349
+ if rdf_bag != nil && rdf_bag.size > 0
1350
+ for tag_node in rdf_bag
1351
+ begin
1352
+ tag_url = XPath.first(root_node, "@resource").to_s
1353
+ tag_match = tag_url.scan(/\/(tag|tags)\/(\w+)/)
1354
+ if tag_match.size > 0
1355
+ @tags << tag_match.first.last.downcase.strip
1356
+ end
1357
+ rescue
1358
+ end
1359
+ end
1360
+ end
1361
+ end
1362
+ if @tags.nil? || @tags.size == 0
1363
+ @tags = []
1364
+ tag_list = XPath.match(root_node, "category/text()")
1365
+ for tag in tag_list
1366
+ @tags << tag.to_s.downcase.strip
1367
+ end
1368
+ end
1369
+ if @tags.nil? || @tags.size == 0
1370
+ @tags = []
1371
+ tag_list = XPath.match(root_node, "dc:subject/text()")
1372
+ for tag in tag_list
1373
+ @tags << tag.to_s.downcase.strip
1374
+ end
1375
+ end
1376
+ if @tags.nil? || @tags.size == 0
1377
+ begin
1378
+ @tags = XPath.first(root_node, "itunes:keywords/text()").to_s.downcase.split(" ")
1379
+ rescue
1380
+ @tags = []
1381
+ end
1382
+ end
1383
+ if @tags.nil?
1384
+ @tags = []
1385
+ end
1386
+ @tags.uniq!
1387
+ end
1388
+ return @tags
1389
+ end
1390
+
1391
+ # Sets the feed item tags
1392
+ def tags=(new_tags)
1393
+ @tags = new_tags
1394
+ end
1395
+
1396
+ # Returns true if this feed item contains explicit material. If the whole
1397
+ # feed has been marked as explicit, this will return true even if the item
1398
+ # isn't explicitly marked as explicit.
1399
+ def explicit?
1400
+ if @explicit.nil?
1401
+ if XPath.first(root_node,
1402
+ "media:adult/text()").to_s.downcase == "true" ||
1403
+ XPath.first(root_node,
1404
+ "itunes:explicit/text()").to_s.downcase == "yes" ||
1405
+ XPath.first(root_node,
1406
+ "itunes:explicit/text()").to_s.downcase == "true" ||
1407
+ feed.explicit?
1408
+ @explicit = true
1409
+ else
1410
+ @explicit = false
1411
+ end
1412
+ end
1413
+ return @explicit
1414
+ end
1415
+
1416
+ # Sets whether or not the feed contains explicit material
1417
+ def explicit=(new_explicit)
1418
+ @explicit = (new_explicit ? true : false)
1419
+ end
1420
+
1421
+ # A hook method that is called during the feed generation process. Overriding this method
1422
+ # will enable additional content to be inserted into the feed.
1423
+ def build_xml_hook(feed_type, version, xml_builder)
1424
+ return nil
1425
+ end
1426
+
1427
+ # Generates xml based on the content of the feed item
1428
+ def build_xml(feed_type=(self.feed.feed_type or "rss"), version=nil,
1429
+ xml_builder=Builder::XmlMarkup.new(:indent => 2))
1430
+ if feed_type == "rss" && (version == nil || version == 0.0)
1431
+ version = 1.0
1432
+ elsif feed_type == "atom" && (version == nil || version == 0.0)
1433
+ version = 0.3
1434
+ end
1435
+ if feed_type == "rss" && (version == 0.9 || version == 1.0 || version == 1.1)
1436
+ # RDF-based rss format
1437
+ if link.nil?
1438
+ raise "Cannot generate an rdf-based feed item with a nil link field."
1439
+ end
1440
+ return xml_builder.item("rdf:about" => CGI.escapeHTML(link)) do
1441
+ unless title.nil? || title == ""
1442
+ xml_builder.title(title)
1443
+ else
1444
+ xml_builder.title
1445
+ end
1446
+ unless link.nil? || link == ""
1447
+ xml_builder.link(link)
1448
+ else
1449
+ xml_builder.link
1450
+ end
1451
+ unless description.nil? || description == ""
1452
+ xml_builder.description(description)
1453
+ else
1454
+ xml_builder.description
1455
+ end
1456
+ unless time.nil?
1457
+ xml_builder.tag!("dc:date", time.iso8601)
1458
+ end
1459
+ unless tags.nil? || tags.size == 0
1460
+ xml_builder.tag!("taxo:topics") do
1461
+ xml_builder.tag!("rdf:Bag") do
1462
+ for tag in tags
1463
+ xml_builder.tag!("rdf:li", tag)
1464
+ end
1465
+ end
1466
+ end
1467
+ xml_builder.tag!("itunes:keywords", tags.join(" "))
1468
+ end
1469
+ build_xml_hook(feed_type, version, xml_builder)
1470
+ end
1471
+ elsif feed_type == "rss"
1472
+ # normal rss format
1473
+ return xml_builder.item do
1474
+ unless title.nil? || title == ""
1475
+ xml_builder.title(title)
1476
+ end
1477
+ unless link.nil? || link == ""
1478
+ xml_builder.link(link)
1479
+ end
1480
+ unless description.nil? || description == ""
1481
+ xml_builder.description(description)
1482
+ end
1483
+ unless time.nil?
1484
+ xml_builder.pubDate(time.rfc822)
1485
+ end
1486
+ unless tags.nil? || tags.size == 0
1487
+ xml_builder.tag!("taxo:topics") do
1488
+ xml_builder.tag!("rdf:Bag") do
1489
+ for tag in tags
1490
+ xml_builder.tag!("rdf:li", tag)
1491
+ end
1492
+ end
1493
+ end
1494
+ xml_builder.tag!("itunes:keywords", tags.join(" "))
1495
+ end
1496
+ build_xml_hook(feed_type, version, xml_builder)
1497
+ end
1498
+ elsif feed_type == "atom" && version == 0.3
1499
+ # normal atom format
1500
+ return xml_builder.entry("xmlns" => "http://purl.org/atom/ns#") do
1501
+ unless title.nil? || title == ""
1502
+ xml_builder.title(title,
1503
+ "mode" => "escaped",
1504
+ "type" => "text/html")
1505
+ end
1506
+ xml_builder.author do
1507
+ unless self.author.nil? || self.author.name.nil?
1508
+ xml_builder.name(self.author.name)
1509
+ else
1510
+ xml_builder.name("n/a")
1511
+ end
1512
+ unless self.author.nil? || self.author.email.nil?
1513
+ xml_builder.email(self.author.email)
1514
+ end
1515
+ unless self.author.nil? || self.author.url.nil?
1516
+ xml_builder.url(self.author.url)
1517
+ end
1518
+ end
1519
+ unless link.nil? || link == ""
1520
+ xml_builder.link("href" => link,
1521
+ "rel" => "alternate",
1522
+ "type" => "text/html",
1523
+ "title" => title)
1524
+ end
1525
+ unless description.nil? || description == ""
1526
+ xml_builder.content(description,
1527
+ "mode" => "escaped",
1528
+ "type" => "text/html")
1529
+ end
1530
+ unless time.nil?
1531
+ xml_builder.issued(time.iso8601)
1532
+ end
1533
+ unless tags.nil? || tags.size == 0
1534
+ for tag in tags
1535
+ xml_builder.category(tag)
1536
+ end
1537
+ end
1538
+ build_xml_hook(feed_type, version, xml_builder)
1539
+ end
1540
+ elsif feed_type == "atom" && version == 1.0
1541
+ # normal atom format
1542
+ return xml_builder.entry("xmlns" => "http://www.w3.org/2005/Atom") do
1543
+ unless title.nil? || title == ""
1544
+ xml_builder.title(title,
1545
+ "type" => "html")
1546
+ end
1547
+ xml_builder.author do
1548
+ unless self.author.nil? || self.author.name.nil?
1549
+ xml_builder.name(self.author.name)
1550
+ else
1551
+ xml_builder.name("n/a")
1552
+ end
1553
+ unless self.author.nil? || self.author.email.nil?
1554
+ xml_builder.email(self.author.email)
1555
+ end
1556
+ unless self.author.nil? || self.author.url.nil?
1557
+ xml_builder.url(self.author.url)
1558
+ end
1559
+ end
1560
+ unless link.nil? || link == ""
1561
+ xml_builder.link("href" => link,
1562
+ "rel" => "alternate",
1563
+ "type" => "text/html",
1564
+ "title" => title)
1565
+ end
1566
+ unless description.nil? || description == ""
1567
+ xml_builder.content(description,
1568
+ "type" => "html")
1569
+ else
1570
+ xml_builder.content(FeedTools.no_content_string,
1571
+ "type" => "html")
1572
+ end
1573
+ if self.updated != nil
1574
+ xml_builder.updated(self.updated.iso8601)
1575
+ elsif self.time != nil
1576
+ # Not technically correct, but a heck of a lot better
1577
+ # than the Time.now fall-back.
1578
+ xml_builder.updated(self.time.iso8601)
1579
+ else
1580
+ xml_builder.updated(Time.now.iso8601)
1581
+ end
1582
+ unless self.published.nil?
1583
+ xml_builder.published(self.published.iso8601)
1584
+ end
1585
+ if self.id != nil
1586
+ unless FeedTools.is_uri? self.id
1587
+ if self.time != nil && self.link != nil
1588
+ xml_builder.id(FeedTools.build_tag_uri(self.link, self.time))
1589
+ elsif self.link != nil
1590
+ xml_builder.id(FeedTools.build_urn_uuid_uri(self.link))
1591
+ else
1592
+ raise "The unique id must be a URI. " +
1593
+ "(Attempted to generate id, but failed.)"
1594
+ end
1595
+ else
1596
+ xml_builder.id(self.id)
1597
+ end
1598
+ elsif self.time != nil && self.link != nil
1599
+ xml_builder.id(FeedTools.build_tag_uri(self.link, self.time))
1600
+ else
1601
+ raise "Cannot build feed, missing feed unique id."
1602
+ end
1603
+ unless self.tags.nil? || self.tags.size == 0
1604
+ for tag in self.tags
1605
+ xml_builder.category("term" => tag)
1606
+ end
1607
+ end
1608
+ build_xml_hook(feed_type, version, xml_builder)
1609
+ end
1610
+ end
1611
+ end
1612
+
1613
+ alias_method :tagline, :description
1614
+ alias_method :tagline=, :description=
1615
+ alias_method :subtitle, :description
1616
+ alias_method :subtitle=, :description=
1617
+ alias_method :summary, :description
1618
+ alias_method :summary=, :description=
1619
+ alias_method :abstract, :description
1620
+ alias_method :abstract=, :description=
1621
+ alias_method :content, :description
1622
+ alias_method :content=, :description=
1623
+ alias_method :guid, :id
1624
+ alias_method :guid=, :id=
1625
+ alias_method :published, :issued
1626
+ alias_method :published=, :issued=
1627
+
1628
+ # Returns a simple representation of the feed item object's state.
1629
+ def inspect
1630
+ return "#<FeedTools::FeedItem:0x#{self.object_id.to_s(16)} " +
1631
+ "LINK:#{self.link}>"
1632
+ end
1633
+ end
1634
+ end