feedtools 0.2.10 → 0.2.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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