RSS Feed Reader

This tutorial outlines an approach for creating a RSS feed reader in Director. It is intended to demonstrate an 'object orientated' approach to creating applications in Director.

What is RSS?

RSS is a 'Web content syndication format'. Basically, it is a dialect of XML. It is used to provide content or summaries of content with links back to the full versions of the content. 'Feed readers' and 'aggregators' are used to check feeds on behalf of a user and display any updated content. For more information, see

RSS File Format

There are various RSS 'standards'. The most common (and still in use) are RSS 0.91, 0.92 and 2.0. We will focus on RSS 2.0, reasoning (like A List Apart) that it is "better to choose a standard and stick with it than to contribute to the fragmentation of the world-wide digital brainosphere".

Sample RSS document:


<?xml version='1.0' ?>
<rss version='2.0'>
  <channel>
	<title>Lingoworkshop</title>
	<link>http://www.lingoworkshop.com/</link>
	<description>News for Director Developers.</description>
	<language>en-au</language>
	<lastBuildDate>Sat, 04 Mar 2006 05:07:51 GMT</lastBuildDate>
	<docs>http://www.lingoworkshop.com/rss.xml</docs>
	<generator>Wrangler 6.4</generator>

    <item>
       <title>Lorem</title>
       <description>Lorem ipsum dolor sit amet</description>
       <link>http://www.lingoworkshop.com/Lorem</link>
       <guid>http://www.lingoworkshop.com/Lorem#article1</guid>
       <pubDate>Thu, 02 Mar 2006 11:09:23 GMT</pubDate>
    </item>

    <item>
       <title>Lipsum</title>
       <description>Lorem ipsum dolor sit amet</description>
       <link>http://www.lingoworkshop.com/Lipsum</link>
       <guid>http://www.lingoworkshop.com/Lipsum#article2</guid>
       <pubDate>Thu, 02 Mar 2006 11:09:23 GMT</pubDate>
    </item>

  </channel>
</rss>

The feed has two main parts to it: Firstly, there is the 'channel information' part. There are various optional elements that provide information about the 'channel' (who is responsible for it, what language it is in, an do so). To keep things simple, we'll focus only on the required elements which are

  • title
  • description
  • link

The next part of the feed is composed of any number of items (up to 15 seems to be the standard). An item is like an article of information. Typically, it is composed of a synopsis with a link back to the full story (although it could be the whole story). There are many optional elements for 'items' - but the only required element is one of title or description. The item elements that our feedreader will look for will be

  • title
  • description
  • link

Sketching out an architecture

Our RSS Reeder is going to have a list of 'subscriptions' (each of which has a name and a url). When we select a subscription, we will get a list of headlines (the title element of the items in the feed). And when we select the headline, we will display the item description - with a link to the full story (provided we can get the link element for that item). We will also work out a system for allowing users to add to the list of subscriptions - and edit the existing ones.

Using a sketch of the GUI as a general guide, we can start to work out some of the main objects.

The SubscriptionMgr will keep a list of subscriptions. It will populate a list box with the title of each of the feeds. When the user selects a feed, the SubscriptionMgr will create a feedObject which will go and download then parse the feed (using a RSS parser). When we have downloaded and successfully parsed the feed, the feedObject will then interact with the GUI to display the feed.

Parsing the XML

Working with XML in Director can be a little awkward, so the first thing we'll do is parse the XML into Director's native PropList format. There are various ways to parse XML in Director, but this example we'll use the XML Xtra that comes with DMX2004 (the older version that came with Director MX will not work because we are going to use the MakePropList method only available in the new version).

Here's a first draft at a basic parser that parses the XML feed into a proplist. It is a fairly simple script with a single method (we could have put this parsing routine into the FeedObject script - but keeping it separate means that we could easily change the parse routine without having to dig through all the other scripts)


-- script("RSS-Parser")

on Load (me, XML)
  data = [#ChannelInfo: [:], #itemsList: [], #error: 0]
  
  XMLParser = new(xtra "xmlparser")
  XMLParser.parseString(XML)
  FeedRaw = XMLParser.makeProplist()
  
  -- check we got a good parse
  if voidP(FeedRaw[#child]) then 
    data.error = 1
    return data
  end if
  if count(FeedRaw[#child]) < 1 then 
    data.error = 1
    return data
  end if
  
  repeat with aChannel in FeedRaw[#child]
    
    if aChannel[#name] = "item" then
      
      thisItem = [:]
      if count(aChannel[#child]) then
        repeat with itemData in aChannel[#child]
          p = symbol(itemData[#name])
          v = itemData[#chardata]
          thisItem.addProp(p, v)
        end repeat
      end if
      data.itemsList.append(thisItem)
      
    else
      
      
      if voidP(aChannel[#child]) then 
        data.error = 1
        return data
      end if
      FirstChannelData = aChannel[#child]
      
      repeat with thing in FirstChannelData
        if thing[#name] = "item" then 
          thisItem = [:]
          if count(thing[#child]) then
            repeat with itemData in thing[#child]
              p = symbol(itemData[#name])
              v = itemData[#chardata]
              thisItem.addProp(p, v)
            end repeat
          end if
          data.itemsList.append(thisItem)
        else
          p = symbol(thing[#name])
          v = thing[#chardata]
          data.channelInfo.addProp(p, v)
        end if
      end repeat
    end if
    
  end repeat
  
  return data
  
end

Don't stress too much about how this script extracts the information from the XML (its probably not doing a very good job of it anyway). All you need to know is that if you call the Load method of this script, passing it the RSS feed (an XML formatted string) as a parameter, it will return a propList with three properties: #ChannelInfo, #itemsList, and #error. The channelInfo is a list of information about the feed (the title, url and description); The itemsList is a list of all the items. The Error property will be false if everything went well.

You can check this script is working by cutting and pasting the RSS XML shown above into a field or text member (eg member("RSS-test-xml")) and in the message window, type the following


parser = script("RSS-Parser")
put parser.Load( member("RSS-test-xml").text)
-- [#channelInfo: [#title: "Lingoworkshop", #link: "http://www.lingoworkshop.com/",...]

 

The next script will be used to create the 'FeedObject'. A feed object will have methods for displaying the data we have read from the XML feed. When we create a new feed object, we will pass a URL for the feed as a parameter. The 'FeedObject' will then create a NetOp object which will download the XML for us. When the XML has been downloaded, the NetOp object will send a #NetTransactionComplete message back to the 'FeedObject', passing the XML string as a parameter. The 'FeedObject' then parses the XML and stores the ChannelInfo and Items list. The 'FeedObject' will provide methods for accessing this data.

Downloading the XML is an asynchronous operation and may take a little while (depending on the size of the XML, the user's connection speed, butterfly activity in Brazil, and so on). Therefore, we will track the current state of the feedObject. For now, we will just store the Object state (ie. 'downloading data', 'got data', 'error getting data', 'error parsing data') in a property called 'StatusStr' and provide a simple method to get this status.

Here is the script:

-- script("RSS-Feed")
		  
property URI
property ChannelInfo
property ItemsList
property StatusStr

on new (me, aURL)
  URI = aURL
  
  -- initialise some properties (these will get
  -- populated when the XML has been parsed)
  ChannelInfo = [:]
  ItemsList = []
  
  -- now download the XML 
  StatusStr = "Getting XML"
  aURL = aURL & "?"&random(the milliseconds)
  NetOp = script("NetOp.transaction").new(aURL)
  NetOp.AddListener(me)
  NetOp.Start()
  return me
end

on NetTransactionComplete (me,sender, netData)
  if netData.error = 0 then
    -- got the net text, now parse it
    XMLStr = netData.text
    RSSObj = script("RSS-Parser").new()
    parseData = RSSObj.Load(XMLStr)
    if parseData.error = 0 then
      -- successfully parsed the XML
      if parseData[#ChannelInfo].ilk = #PropList then ChannelInfo = parseData[#ChannelInfo]
      if parseData[#itemsList].ilk = #List then ItemsList = parseData[#itemsList]
      StatusStr = "Done"
      
    else
      -- error parsing
      StatusStr = "Error parsing the XML feed"
    end if
  else
    -- error getting the xml
    StatusStr = "Error retreiving the XML feed - " & sender.GetErrorDescription(netData.error)
  end if
end 

on NetTransactionStatusUpdate (me,sender,data)
  StatusStr = data[#state]
  if StatusStr = "InProgress" then 
    StatusStr = "Downloading" && integer(data[#fractiondone]*100) & "%"
  end if
end

-- Some Accessors for the GUI

on GetStatus (me)
  return StatusStr
end

on GetFeedURL (me)
  return URI
end

on GetChannelInfo (me)
  return channelInfo
end

on GetTitleList (me)
  rList = []
  repeat with anItem in itemsList
    rList.append(anItem[#title])
  end repeat
  return rList
end

on GetItemAt (me,p)
  return itemsList[p]
end

As mentioned previously, this script uses a NetOp object from the Xlib to download the XML. The XML Parser Xtra can parse URLs, however I prefer to download the XML first (using GetNetText) since the Network Xtras give you more information on downloads (errors, percentage downloaded etc).

For more information on the internal workings of the NetOp scripts, see this article - but for the purposes of this project, just note that they act as self-sufficient 'daemons' - objects that work in the background. In this example, we are using the "NetOp.transaction" script to download some text from a URL. When the transaction is complete, the daemon sends a NetTransactionComplete message to its listeners, passing the results of the transaction as a parameter. The FeedObject adds itself as a listener when it creates the NetOp.

To check everything is working so far, create a new movie, copy the NetOp scripts from from the Xlib and create the "RSS-Feed" and "RSS-Parser" scripts described here. Put a basic "go to the frame" behaviour in the first frame, and whilst the movie is playing, type the following into the message window:


feed = script("RSS-Feed").new("http://www.lingoworkshop.com/rss.xml")

The movie needs to be playing so Director services the timeouts and the network operation. Wait a few moments (for the XML to download), and then type


put feed.GetChannelInfo()
-- [#title: "Lingoworkshop", #link: "http://www.lingoworkshop.com/", ... etc]
put feed.GetTitleList()
-- ["Using Javascript", "Standard Practice and 'Rules of Thumb'", ... etc]

The next script is the SubscriptionMgr. Basically, it keeps a list of feed urls. When a feed is selected, it creates a FeedObject for that URL. Here's the script:


property SubscriptionList
property OpenFeeds

on new (me)
  
  SubscriptionList = me.GetSavedSubscriptions()
  OpenFeeds = [:]
  ActiveFeed = VOID
  
  return me
end

on GetSubscriptions (me)
  rList = []
  repeat with aSite in SubscriptionList
    rList.append(aSite.name)
  end repeat
  return rList
end

on OpenSubscription (me, pos)
  -- error check the parameter
  if not integerP(pos) then pos = 1
  if pos < 1 or pos > SubscriptionList.count then return #ParameterError_IndexOutOfRange
  -- have we already created a feedOBj for this Url?
  site = SubscriptionList[pos]
  feed = OpenFeeds.getAProp(site.url)
  if feed.ilk <> #instance then
    -- haven't created it yet
    feed = script("RSS-Feed").new(site.url)
    OpenFeeds.AddProp(site.url,feed)
  end if
  return feed
end

on OpenURL (me, url)
  -- have we already created a feedOBj for this Url?
  feed = OpenFeeds.getAProp(url)
  if feed.ilk <> #instance then
    -- haven't created it yet
    feed = script("RSS-Feed").new(url)
    OpenFeeds.AddProp(url,feed)
  end if
  return feed
end

on GetSavedSubscriptions (me)
  localFile = "SW_RSS_READER.txt"
  ListSaver = script("ListSaver").new()
  subsList = ListSaver.ReadList(localFile)
  if voidP(subsList) then 
    -- use defaults
    subsList = me.GetDefaultList()
    ListSaver.SaveList(subsList, localFile)
    
  end if
  return subsList
end

on GetDefaultList (me)
  subsList = []
  subsList[1] = [#name: "Lingoworkshop", #Url: "http://www.lingoworkshop.com/rss.xml"]
  subsList[2] = [#name: "Director@Night", #Url: "http://www.directoratnight.com/feed/"]
  subsList[3] = [#name: "Tom Higgans Blog", #Url: "http://weblogs.macromedia.com/thiggins/index.xml"]
  subsList[4] = [#name: "Director Online (Articles)", #Url: "http://director-online.com/rss/director-online-articles.xml"]
  subsList[5] = [#name: "Farbflash", #Url: "http://www.farbflash.de/cgi-bin/blosxom.cgi/index.rss"]
  subsList[6] = [#name: "Updatestage", #Url: "http://www.updatestage.com/rss/new.rss"]
  subsList[7] = [#name: "Director-Web", #Url: "http://www.mcli.dist.maricopa.edu/director/feed/new.rss"]
  return subsList
end

Note - this script uses a the "ListSaver" script from the Xlib to save the 'subscription' list to a local file. It is not important here to understand the internal workings of that script - just that you can save a list like this:


ListSaver = script("ListSaver").new()
-- Read a list
someList = ListSaver.ReadList(localFile)
-- Save a list
ListSaver.SaveList(someList, localFilePath)

To check this is all working, type the following into the message window:


subMgr = script("SubscriptionMgr").new()
feed = subMgr.OpenSubscription(3)
put feed.GetChannelInfo()
-- [#title: "Tom Higgins", #link: "http://weblogs.macromedia.com/thiggins/", ... etc]

If you get an empty list when you request the channelInfo, it probably means that the XML is still being downloaded. You can do a quick check by typing "put feed.GetStatus()" into the message window.

Summary of Part 1

So far, we have created a basic RSS Reader that we can interact with via the message window. The next step is to create a graphic user interface.

Downloads

Here is a source movie containing the scripts discussed.

First published 10/03/2006